/*
 * Copyright 2019 Flipkart Internet Pvt. Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.flipkart.android.proteus.gson;

import android.content.Context;

import com.flipkart.android.proteus.FunctionManager;
import com.flipkart.android.proteus.Proteus;
import com.flipkart.android.proteus.ProteusConstants;
import com.flipkart.android.proteus.ViewTypeParser;
import com.flipkart.android.proteus.processor.AttributeProcessor;
import com.flipkart.android.proteus.value.Array;
import com.flipkart.android.proteus.value.Binding;
import com.flipkart.android.proteus.value.Layout;
import com.flipkart.android.proteus.value.Null;
import com.flipkart.android.proteus.value.ObjectValue;
import com.flipkart.android.proteus.value.Primitive;
import com.flipkart.android.proteus.value.Value;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.JsonReaderInternalAccess;
import com.google.gson.internal.LazilyParsedNumber;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

/**
 * ProteusTypeAdapterFactory
 *
 * @author aditya.sharat
 */
public class ProteusTypeAdapterFactory implements TypeAdapterFactory {

  public static final ProteusInstanceHolder PROTEUS_INSTANCE_HOLDER = new ProteusInstanceHolder();

  Context context;

  /**
   *
   */
  public final TypeAdapter<Value> VALUE_TYPE_ADAPTER = new TypeAdapter<Value>() {
    @Override
    public void write(JsonWriter out, Value value) throws IOException {
      throw new UnsupportedOperationException("Use ProteusTypeAdapterFactory.COMPILED_VALUE_TYPE_ADAPTER instead");
    }

    @Override
    public Value read(JsonReader in) throws IOException {
      switch (in.peek()) {
        case STRING:
          return compileString(getContext(), in.nextString());
        case NUMBER:
          String number = in.nextString();
          return new Primitive(new LazilyParsedNumber(number));
        case BOOLEAN:
          return new Primitive(in.nextBoolean());
        case NULL:
          in.nextNull();
          return Null.INSTANCE;
        case BEGIN_ARRAY:
          Array array = new Array();
          in.beginArray();
          while (in.hasNext()) {
            array.add(read(in));
          }
          in.endArray();
          return array;
        case BEGIN_OBJECT:
          ObjectValue object = new ObjectValue();
          in.beginObject();
          if (in.hasNext()) {
            String name = in.nextName();
            if (ProteusConstants.TYPE.equals(name) && JsonToken.STRING.equals(in.peek())) {
              String type = in.nextString();
              if (PROTEUS_INSTANCE_HOLDER.isLayout(type)) {
                Layout layout = LAYOUT_TYPE_ADAPTER.read(type, PROTEUS_INSTANCE_HOLDER.getProteus(), in);
                in.endObject();
                return layout;
              } else {
                object.add(name, compileString(getContext(), type));
              }
            } else {
              object.add(name, read(in));
            }
          }
          while (in.hasNext()) {
            object.add(in.nextName(), read(in));
          }
          in.endObject();
          return object;
        case END_DOCUMENT:
        case NAME:
        case END_OBJECT:
        case END_ARRAY:
        default:
          throw new IllegalArgumentException();
      }
    }
  }.nullSafe();

  /**
   *
   */
  public final TypeAdapter<Primitive> PRIMITIVE_TYPE_ADAPTER = new TypeAdapter<Primitive>() {

    @Override
    public void write(JsonWriter out, Primitive value) throws IOException {
      VALUE_TYPE_ADAPTER.write(out, value);
    }

    @Override
    public Primitive read(JsonReader in) throws IOException {
      Value value = VALUE_TYPE_ADAPTER.read(in);
      return value != null && value.isPrimitive() ? value.getAsPrimitive() : null;
    }
  }.nullSafe();

  /**
   *
   */
  public final TypeAdapter<ObjectValue> OBJECT_TYPE_ADAPTER = new TypeAdapter<ObjectValue>() {
    @Override
    public void write(JsonWriter out, ObjectValue value) throws IOException {
      VALUE_TYPE_ADAPTER.write(out, value);
    }

    @Override
    public ObjectValue read(JsonReader in) throws IOException {
      Value value = VALUE_TYPE_ADAPTER.read(in);
      return value != null && value.isObject() ? value.getAsObject() : null;
    }
  }.nullSafe();

  /**
   *
   */
  public final TypeAdapter<Array> ARRAY_TYPE_ADAPTER = new TypeAdapter<Array>() {
    @Override
    public void write(JsonWriter out, Array value) throws IOException {
      VALUE_TYPE_ADAPTER.write(out, value);
    }

    @Override
    public Array read(JsonReader in) throws IOException {
      Value value = VALUE_TYPE_ADAPTER.read(in);
      return value != null && value.isArray() ? value.getAsArray() : null;
    }
  }.nullSafe();

  /**
   *
   */
  public final TypeAdapter<Null> NULL_TYPE_ADAPTER = new TypeAdapter<Null>() {

    @Override
    public void write(JsonWriter out, Null value) throws IOException {
      VALUE_TYPE_ADAPTER.write(out, value);
    }

    @Override
    public Null read(JsonReader in) throws IOException {
      Value value = VALUE_TYPE_ADAPTER.read(in);
      return value != null && value.isNull() ? value.getAsNull() : null;
    }
  }.nullSafe();

  /**
   *
   */
  public final LayoutTypeAdapter LAYOUT_TYPE_ADAPTER = new LayoutTypeAdapter();

  /**
   *
   */
  public final TypeAdapter<Value> COMPILED_VALUE_TYPE_ADAPTER = new TypeAdapter<Value>() {

    public static final String TYPE = "$t";
    public static final String VALUE = "$v";

    @Override
    public void write(JsonWriter out, Value value) throws IOException {
      if (value == null || value.isNull()) {
        out.nullValue();
      } else if (value.isPrimitive()) {
        Primitive primitive = value.getAsPrimitive();
        if (primitive.isNumber()) {
          out.value(primitive.getAsNumber());
        } else if (primitive.isBoolean()) {
          out.value(primitive.getAsBoolean());
        } else {
          out.value(primitive.getAsString());
        }
      } else if (value.isObject()) {
        out.beginObject();
        for (Map.Entry<String, Value> e : value.getAsObject().entrySet()) {
          out.name(e.getKey());
          write(out, e.getValue());
        }
        out.endObject();
      } else if (value.isArray()) {
        out.beginArray();
        Iterator<Value> iterator = value.getAsArray().iterator();
        while (iterator.hasNext()) {
          write(out, iterator.next());
        }
        out.endArray();
      } else {
        CustomValueTypeAdapter adapter = getCustomValueTypeAdapter(value.getClass());

        out.beginObject();

        out.name(TYPE);
        out.value(adapter.type);

        out.name(VALUE);

        //noinspection unchecked
        adapter.write(out, value);

        out.endObject();
      }
    }

    @Override
    public Value read(JsonReader in) throws IOException {
      switch (in.peek()) {
        case STRING:
          return compileString(getContext(), in.nextString());
        case NUMBER:
          String number = in.nextString();
          return new Primitive(new LazilyParsedNumber(number));
        case BOOLEAN:
          return new Primitive(in.nextBoolean());
        case NULL:
          in.nextNull();
          return Null.INSTANCE;
        case BEGIN_ARRAY:
          Array array = new Array();
          in.beginArray();
          while (in.hasNext()) {
            array.add(read(in));
          }
          in.endArray();
          return array;
        case BEGIN_OBJECT:
          ObjectValue object = new ObjectValue();
          in.beginObject();
          if (in.hasNext()) {
            String name = in.nextName();
            if (TYPE.equals(name) && JsonToken.NUMBER.equals(in.peek())) {
              int type = Integer.parseInt(in.nextString());
              CustomValueTypeAdapter<? extends Value> adapter = getCustomValueTypeAdapter(type);
              in.nextName();
              Value value = adapter.read(in);
              in.endObject();
              return value;
            } else {
              object.add(name, read(in));
            }
          }
          while (in.hasNext()) {
            object.add(in.nextName(), read(in));
          }
          in.endObject();
          return object;
        case END_DOCUMENT:
        case NAME:
        case END_OBJECT:
        case END_ARRAY:
        default:
          throw new IllegalArgumentException();
      }
    }

  };


  /**
   *
   */
  private CustomValueTypeAdapterMap map = new CustomValueTypeAdapterMap();

  public static final String ARRAYS_DELIMITER = "|";
  public static final String ARRAY_DELIMITER = ",";

  public static String writeArrayOfInts(int[] array) {
    StringBuilder builder = new StringBuilder();
    for (int index = 0; index < array.length; index++) {
      builder.append(array[index]);
      if (index < array.length - 1) {
        builder.append(ARRAY_DELIMITER);
      }
    }
    return builder.toString();
  }

  public static String writeArrayOfIntArrays(int[][] arrays) {
    StringBuilder builder = new StringBuilder();
    for (int index = 0; index < arrays.length; index++) {
      builder.append(writeArrayOfInts(arrays[index]));
      if (index < arrays.length - 1) {
        builder.append(ARRAYS_DELIMITER);
      }
    }
    return builder.toString();
  }

  public static int[] readArrayOfInts(String string) {
    int[] array = new int[0];
    StringTokenizer tokenizer = new StringTokenizer(string, ARRAY_DELIMITER);
    while (tokenizer.hasMoreTokens()) {
      array = Arrays.copyOf(array, array.length + 1);
      array[array.length - 1] = Integer.parseInt(tokenizer.nextToken());
    }
    return array;
  }

  public static int[][] readArrayOfIntArrays(String string) {
    int[][] arrays = new int[0][];
    StringTokenizer tokenizer = new StringTokenizer(string, ARRAYS_DELIMITER);
    while (tokenizer.hasMoreTokens()) {
      arrays = Arrays.copyOf(arrays, arrays.length + 1);
      arrays[arrays.length - 1] = readArrayOfInts(tokenizer.nextToken());
    }
    return arrays;
  }

  /**
   * @param context
   */
  public ProteusTypeAdapterFactory(Context context) {
    this.context = context;
    DefaultModule.create().register(this);
  }

  @Override
  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
    Class clazz = type.getRawType();

    if (clazz == Primitive.class) {
      //noinspection unchecked
      return (TypeAdapter<T>) PRIMITIVE_TYPE_ADAPTER;
    } else if (clazz == ObjectValue.class) {
      //noinspection unchecked
      return (TypeAdapter<T>) OBJECT_TYPE_ADAPTER;
    } else if (clazz == Array.class) {
      //noinspection unchecked
      return (TypeAdapter<T>) ARRAY_TYPE_ADAPTER;
    } else if (clazz == Null.class) {
      //noinspection unchecked
      return (TypeAdapter<T>) NULL_TYPE_ADAPTER;
    } else if (clazz == Layout.class) {
      //noinspection unchecked
      return (TypeAdapter<T>) LAYOUT_TYPE_ADAPTER;
    } else if (clazz == Value.class) {
      //noinspection unchecked
      return (TypeAdapter<T>) VALUE_TYPE_ADAPTER;
    }

    return null;
  }

  public void register(Class<? extends Value> clazz, CustomValueTypeAdapterCreator<? extends Value> creator) {
    map.register(clazz, creator);
  }

  public CustomValueTypeAdapter<? extends Value> getCustomValueTypeAdapter(Class<? extends Value> clazz) {
    return map.get(clazz);
  }

  public CustomValueTypeAdapter<? extends Value> getCustomValueTypeAdapter(int type) {
    return map.get(type);
  }

  public Context getContext() {
    return context;
  }

  static Value compileString(Context context, String string) {
    if (Binding.isBindingValue(string)) {
      return Binding.valueOf(string, context, PROTEUS_INSTANCE_HOLDER.getProteus().functions);
    } else {
      return new Primitive(string);
    }
  }

  public interface Module {

    /**
     * @param factory
     */
    void register(ProteusTypeAdapterFactory factory);

  }

  public static class ProteusInstanceHolder {

    private Proteus proteus;

    ProteusInstanceHolder() {
    }

    public Proteus getProteus() {
      return proteus;
    }

    public void setProteus(Proteus proteus) {
      this.proteus = proteus;
    }

    public boolean isLayout(String type) {
      return null != proteus && proteus.has(type);
    }
  }

  public class LayoutTypeAdapter extends TypeAdapter<Layout> {

    @Override
    public void write(JsonWriter out, Layout value) throws IOException {
      VALUE_TYPE_ADAPTER.write(out, value);
    }

    @Override
    public Layout read(JsonReader in) throws IOException {
      Value value = VALUE_TYPE_ADAPTER.read(in);
      return value != null && value.isLayout() ? value.getAsLayout() : null;
    }

    public Layout read(String type, Proteus proteus, JsonReader in) throws IOException {
      List<Layout.Attribute> attributes = new ArrayList<>();
      Map<String, Value> data = null;
      ObjectValue extras = new ObjectValue();
      String name;
      while (in.hasNext()) {
        name = in.nextName();
        if (ProteusConstants.DATA.equals(name)) {
          data = readData(in);
        } else {
          ViewTypeParser.AttributeSet.Attribute attribute = proteus.getAttributeId(name, type);
          if (null != attribute) {
            FunctionManager manager = PROTEUS_INSTANCE_HOLDER.getProteus().functions;
            Value value = attribute.processor.precompile(VALUE_TYPE_ADAPTER.read(in), getContext(), manager);
            attributes.add(new Layout.Attribute(attribute.id, value));
          } else {
            extras.add(name, VALUE_TYPE_ADAPTER.read(in));
          }
        }
      }
      return new Layout(type, attributes.size() > 0 ? attributes : null, data, extras.entrySet().size() > 0 ? extras : null);
    }

    public Map<String, Value> readData(JsonReader in) throws IOException {
      JsonToken peek = in.peek();
      if (peek == JsonToken.NULL) {
        in.nextNull();
        return new HashMap<>();
      }

      if (peek != JsonToken.BEGIN_OBJECT) {
        throw new JsonSyntaxException("data must be a Map<String, String>.");
      }

      Map<String, Value> data = new LinkedHashMap<>();

      in.beginObject();
      while (in.hasNext()) {
        JsonReaderInternalAccess.INSTANCE.promoteNameToValue(in);
        String key = in.nextString();
        Value value = VALUE_TYPE_ADAPTER.read(in);
        Value compiled = AttributeProcessor.staticPreCompile(value, context, PROTEUS_INSTANCE_HOLDER.getProteus().functions);
        if (compiled != null) {
          value = compiled;
        }
        Value replaced = data.put(key, value);
        if (replaced != null) {
          throw new JsonSyntaxException("duplicate key: " + key);
        }
      }
      in.endObject();

      return data;
    }
  }

  private class CustomValueTypeAdapterMap {

    private final Map<Class<? extends Value>, CustomValueTypeAdapter<? extends Value>> types = new HashMap<>();

    private CustomValueTypeAdapter<? extends Value>[] adapters = new CustomValueTypeAdapter[0];

    CustomValueTypeAdapterMap() {
    }

    public CustomValueTypeAdapter<? extends Value> register(Class<? extends Value> clazz, CustomValueTypeAdapterCreator creator) {
      CustomValueTypeAdapter<? extends Value> adapter = types.get(clazz);
      if (null != adapter) {
        return adapter;
      }
      //noinspection unchecked
      adapter = creator.create(adapters.length, ProteusTypeAdapterFactory.this);
      adapters = Arrays.copyOf(adapters, adapters.length + 1);
      adapters[adapters.length - 1] = adapter;
      return types.put(clazz, adapter);
    }

    public CustomValueTypeAdapter<? extends Value> get(Class<? extends Value> clazz) {
      CustomValueTypeAdapter i = types.get(clazz);
      if (null == i) {
        throw new IllegalArgumentException(clazz.getName() + " is not a known value type! Remember to register the class first");
      }
      return types.get(clazz);
    }

    public CustomValueTypeAdapter<? extends Value> get(int i) {
      if (i < adapters.length) {
        return adapters[i];
      }
      throw new IllegalArgumentException(i + " is not a known value type! Did you conjure up this int?");
    }
  }
}