/*
 * Copyright (C) 2016 Bradley Campbell.
 *
 * 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 paperparcel;

import com.google.auto.common.MoreTypes;
import com.google.auto.value.AutoValue;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.squareup.javapoet.TypeName;
import java.util.List;
import java.util.Map;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import paperparcel.TypeKey.*;

/**
 * Responsible for caching the qualified class names of all of the TypeAdapters discovered across
 * the various processing rounds and also cache {@link AdapterDescriptor} instances as these can be
 * expensive to create.
 */
final class AdapterRegistry {
  private static final TypeKey STRING = ClassKey.get("java.lang.String");
  private static final TypeKey INTEGER = ClassKey.get("java.lang.Integer");
  private static final TypeKey MAP =
      ParameterizedKey.get(ClassKey.get("java.util.Map"), asList(AnyKey.get("K"), AnyKey.get("V")));
  private static final TypeKey BUNDLE = ClassKey.get("android.os.Bundle");
  private static final TypeKey PERSISTABLE_BUNDLE = ClassKey.get("android.os.PersistableBundle");
  private static final TypeKey PARCELABLE =
      BoundedKey.get("T", asList(ClassKey.get("android.os.Parcelable")));
  private static final TypeKey SHORT = ClassKey.get("java.lang.Short");
  private static final TypeKey LONG = ClassKey.get("java.lang.Long");
  private static final TypeKey FLOAT = ClassKey.get("java.lang.Float");
  private static final TypeKey DOUBLE = ClassKey.get("java.lang.Double");
  private static final TypeKey BOOLEAN = ClassKey.get("java.lang.Boolean");
  private static final TypeKey CHAR_SEQUENCE = ClassKey.get("java.lang.CharSequence");
  private static final TypeKey LIST =
      ParameterizedKey.get(ClassKey.get("java.util.List"), asList(AnyKey.get("T")));
  private static final TypeKey SPARSE_ARRAY =
      ParameterizedKey.get(ClassKey.get("android.util.SparseArray"), asList(AnyKey.get("T")));
  private static final TypeKey BOOLEAN_ARRAY = PrimitiveArrayKey.BOOLEAN_ARRAY;
  private static final TypeKey BYTE_ARRAY = PrimitiveArrayKey.BYTE_ARRAY;
  private static final TypeKey IBINDER = ClassKey.get("android.os.IBinder");
  private static final TypeKey INT_ARRAY = PrimitiveArrayKey.INT_ARRAY;
  private static final TypeKey LONG_ARRAY = PrimitiveArrayKey.LONG_ARRAY;
  private static final TypeKey BYTE = ClassKey.get("java.lang.Byte");
  private static final TypeKey SIZE = ClassKey.get("android.util.Size");
  private static final TypeKey SIZE_F = ClassKey.get("android.util.SizeF");
  private static final TypeKey DOUBLE_ARRAY = PrimitiveArrayKey.DOUBLE_ARRAY;
  private static final TypeKey SPARSE_BOOLEAN_ARRAY = ClassKey.get("android.util.SparseBooleanArray");
  private static final TypeKey COLLECTION =
      ParameterizedKey.get(ClassKey.get("java.util.Collection"), asList(AnyKey.get("T")));
  private static final TypeKey OBJECT_ARRAY = ArrayKey.of(AnyKey.get("T"));
  private static final TypeKey SET =
      ParameterizedKey.get(ClassKey.get("java.util.Set"), asList(AnyKey.get("T")));
  private static final TypeKey CHAR_ARRAY = PrimitiveArrayKey.CHAR_ARRAY;
  private static final TypeKey FLOAT_ARRAY = PrimitiveArrayKey.FLOAT_ARRAY;
  private static final TypeKey SHORT_ARRAY = PrimitiveArrayKey.SHORT_ARRAY;
  private static final TypeKey CHARACTER = ClassKey.get("java.lang.Character");
  private static final TypeKey ENUM =
      BoundedKey.get("T", asList(
          ParameterizedKey.get(ClassKey.get("java.lang.Enum"), asList(AnyKey.get("T")))));
  private static final TypeKey SERIALIZABLE =
      BoundedKey.get("T", asList(ClassKey.get("java.io.Serializable")));

  private static final String STATIC_ADAPTERS = "paperparcel.internal.StaticAdapters";

  private static ImmutableList<TypeKey> asList(TypeKey... keys) {
    ImmutableList.Builder<TypeKey> builder = ImmutableList.builder();
    for (TypeKey key : keys) {
      builder.add(key);
    }
    return builder.build();
  }

  static abstract class Entry {
    abstract TypeKey typeKey();
    abstract boolean nullSafe();
  }

  @AutoValue
  static abstract class ClassEntry extends Entry {
    abstract String qualifiedName();

    static ClassEntry create(
        String qualifiedName, TypeKey key, boolean nullSafe) {
      return new AutoValue_AdapterRegistry_ClassEntry(key, nullSafe, qualifiedName);
    }
  }

  @AutoValue
  static abstract class FieldEntry extends Entry {
    abstract String enclosingClass();
    abstract String fieldName();

    static FieldEntry create(
        String enclosingClass, String fieldName, TypeKey key, boolean nullSafe) {
      return new AutoValue_AdapterRegistry_FieldEntry(key, nullSafe, enclosingClass, fieldName);
    }
  }

  private static final ImmutableList<Entry> BUILT_IN_ADAPTER_ENTRIES = ImmutableList.of(
      FieldEntry.create(STATIC_ADAPTERS, "STRING_ADAPTER", STRING, true),
      FieldEntry.create(STATIC_ADAPTERS, "INTEGER_ADAPTER", INTEGER, false),
      ClassEntry.create("paperparcel.internal.MapAdapter", MAP, false),
      FieldEntry.create(STATIC_ADAPTERS, "BUNDLE_ADAPTER", BUNDLE, true),
      FieldEntry.create(STATIC_ADAPTERS, "PERSISTABLE_BUNDLE_ADAPTER", PERSISTABLE_BUNDLE, true),
      ClassEntry.create("paperparcel.internal.ParcelableAdapter", PARCELABLE, true),
      FieldEntry.create(STATIC_ADAPTERS, "SHORT_ADAPTER", SHORT, false),
      FieldEntry.create(STATIC_ADAPTERS, "LONG_ADAPTER", LONG, false),
      FieldEntry.create(STATIC_ADAPTERS, "FLOAT_ADAPTER", FLOAT, false),
      FieldEntry.create(STATIC_ADAPTERS, "DOUBLE_ADAPTER", DOUBLE, false),
      FieldEntry.create(STATIC_ADAPTERS, "BOOLEAN_ADAPTER", BOOLEAN, false),
      FieldEntry.create(STATIC_ADAPTERS, "CHAR_SEQUENCE_ADAPTER", CHAR_SEQUENCE, true),
      ClassEntry.create("paperparcel.internal.ListAdapter", LIST, false),
      ClassEntry.create("paperparcel.internal.SparseArrayAdapter", SPARSE_ARRAY, false),
      FieldEntry.create(STATIC_ADAPTERS, "BOOLEAN_ARRAY_ADAPTER", BOOLEAN_ARRAY, true),
      FieldEntry.create(STATIC_ADAPTERS, "BYTE_ARRAY_ADAPTER", BYTE_ARRAY, true),
      FieldEntry.create(STATIC_ADAPTERS, "IBINDER_ADAPTER", IBINDER, true),
      FieldEntry.create(STATIC_ADAPTERS, "INT_ARRAY_ADAPTER", INT_ARRAY, true),
      FieldEntry.create(STATIC_ADAPTERS, "LONG_ARRAY_ADAPTER", LONG_ARRAY, true),
      FieldEntry.create(STATIC_ADAPTERS, "BYTE_ADAPTER", BYTE, false),
      FieldEntry.create(STATIC_ADAPTERS, "SIZE_ADAPTER", SIZE, false),
      FieldEntry.create(STATIC_ADAPTERS, "SIZE_F_ADAPTER", SIZE_F, false),
      FieldEntry.create(STATIC_ADAPTERS, "DOUBLE_ARRAY_ADAPTER", DOUBLE_ARRAY, true),
      FieldEntry.create(STATIC_ADAPTERS, "SPARSE_BOOLEAN_ARRAY_ADAPTER", SPARSE_BOOLEAN_ARRAY, true),
      ClassEntry.create("paperparcel.internal.CollectionAdapter", COLLECTION, false),
      ClassEntry.create("paperparcel.internal.SetAdapter", SET, false),
      FieldEntry.create(STATIC_ADAPTERS, "CHAR_ARRAY_ADAPTER", CHAR_ARRAY, true),
      FieldEntry.create(STATIC_ADAPTERS, "FLOAT_ARRAY_ADAPTER", FLOAT_ARRAY, true),
      FieldEntry.create(STATIC_ADAPTERS, "SHORT_ARRAY_ADAPTER", SHORT_ARRAY, false),
      FieldEntry.create(STATIC_ADAPTERS, "CHARACTER_ADAPTER", CHARACTER, false),
      ClassEntry.create("paperparcel.internal.EnumAdapter", ENUM, false),
      ClassEntry.create("paperparcel.internal.ArrayAdapter", OBJECT_ARRAY, false),
      // Serializable must be last.
      ClassEntry.create("paperparcel.internal.SerializableAdapter", SERIALIZABLE, false));

  private final List<Entry> entries = Lists.newArrayList(BUILT_IN_ADAPTER_ENTRIES);
  private final Map<TypeName, AdapterDescriptor> adapters = Maps.newLinkedHashMap();

  private final Elements elements;
  private final Types types;

  AdapterRegistry(
      Elements elements,
      Types types) {
    this.elements = elements;
    this.types = types;
  }

  void addClassEntry(TypeElement element, boolean nullSafe) {
    String qualifiedName = element.getQualifiedName().toString();
    TypeKey key = TypeKey.get(
        Utils.getAdaptedType(elements, types, MoreTypes.asDeclared(element.asType())));
    Entry entry = ClassEntry.create(qualifiedName, key, nullSafe);
    entries.add(0, entry);
  }

  boolean contains(TypeElement element) {
    for (Entry entry : entries) {
      if (entry instanceof ClassEntry) {
        if (element.getQualifiedName().contentEquals(((ClassEntry) entry).qualifiedName())) {
          return true;
        }
      }
    }
    return false;
  }

  List<Entry> getEntries(boolean allowSerializable) {
    if (allowSerializable) {
      return entries;
    } else {
      return entries.subList(0, entries.size() - 1);
    }
  }

  void registerAdapterFor(TypeName fieldType, AdapterDescriptor adapter) {
    adapters.put(fieldType, adapter);
  }

  Optional<AdapterDescriptor> getAdapterFor(TypeName fieldType) {
    return Optional.fromNullable(adapters.get(fieldType));
  }
}
