/*
 * Copyright (C) 2015 Google, Inc.
 *
 * 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 dagger.internal.codegen;

import com.google.auto.common.MoreTypes;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import dagger.MapKey;
import dagger.internal.codegen.writer.ClassName;
import dagger.internal.codegen.writer.Snippet;
import dagger.internal.codegen.writer.TypeName;
import dagger.internal.codegen.writer.TypeNames;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.SimpleAnnotationValueVisitor6;
import javax.lang.model.util.SimpleTypeVisitor6;
import javax.lang.model.util.Types;

import static com.google.auto.common.AnnotationMirrors.getAnnotatedAnnotations;
import static com.google.auto.common.AnnotationMirrors.getAnnotationValuesWithDefaults;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.collect.Iterables.transform;
import static dagger.internal.codegen.writer.Snippet.makeParametersSnippet;
import static javax.lang.model.util.ElementFilter.methodsIn;

/**
 * Methods for extracting {@link MapKey} annotations and key snippets from binding elements.
 */
final class MapKeys {

  /**
   * If {@code bindingElement} is annotated with a {@link MapKey} annotation, returns it.
   *
   * @throws IllegalArgumentException if the element is annotated with more than one {@code MapKey}
   *     annotation
   */
  static Optional<? extends AnnotationMirror> getMapKey(Element bindingElement) {
    ImmutableSet<? extends AnnotationMirror> mapKeys = getMapKeys(bindingElement);
    return mapKeys.isEmpty()
        ? Optional.<AnnotationMirror>absent()
        : Optional.of(getOnlyElement(mapKeys));
  }

  /**
   * Returns all of the {@link MapKey} annotations that annotate {@code bindingElement}.
   */
  static ImmutableSet<? extends AnnotationMirror> getMapKeys(Element bindingElement) {
    return getAnnotatedAnnotations(bindingElement, MapKey.class);
  }

  /**
   * Returns the annotation value if {@code mapKey}'s type is annotated with
   * {@link MapKey @MapKey(unwrapValue = true)}.
   *
   * @throws IllegalArgumentException if {@code mapKey}'s type is not annotated with
   *     {@link MapKey @MapKey} at all.
   */
  static Optional<? extends AnnotationValue> unwrapValue(AnnotationMirror mapKey) {
    MapKey mapKeyAnnotation = mapKey.getAnnotationType().asElement().getAnnotation(MapKey.class);
    checkArgument(
        mapKeyAnnotation != null, "%s is not annotated with @MapKey", mapKey.getAnnotationType());
    return mapKeyAnnotation.unwrapValue()
        ? Optional.of(getOnlyElement(mapKey.getElementValues().values()))
        : Optional.<AnnotationValue>absent();
  }

  /**
   * Returns the map key type for an unwrapped {@link MapKey} annotation type. If the single member
   * type is primitive, returns the boxed type.
   *
   * @throws IllegalArgumentException if {@code mapKeyAnnotationType} is not an annotation type or
   *     has more than one member, or if its single member is an array
   * @throws NoSuchElementException if the annotation has no members
   */
  public static DeclaredType getUnwrappedMapKeyType(
      final DeclaredType mapKeyAnnotationType, final Types types) {
    checkArgument(
        MoreTypes.asTypeElement(mapKeyAnnotationType).getKind() == ElementKind.ANNOTATION_TYPE,
        "%s is not an annotation type",
        mapKeyAnnotationType);

    final ExecutableElement onlyElement =
        getOnlyElement(methodsIn(mapKeyAnnotationType.asElement().getEnclosedElements()));

    SimpleTypeVisitor6<DeclaredType, Void> keyTypeElementVisitor =
        new SimpleTypeVisitor6<DeclaredType, Void>() {

          @Override
          public DeclaredType visitArray(ArrayType t, Void p) {
            throw new IllegalArgumentException(
                mapKeyAnnotationType + "." + onlyElement.getSimpleName() + " cannot be an array");
          }

          @Override
          public DeclaredType visitPrimitive(PrimitiveType t, Void p) {
            return MoreTypes.asDeclared(types.boxedClass(t).asType());
          }

          @Override
          public DeclaredType visitDeclared(DeclaredType t, Void p) {
            return t;
          }
        };
    return keyTypeElementVisitor.visit(onlyElement.getReturnType());
  }

  /**
   * Returns the name of the generated class that contains the static {@code create} methods for a
   * {@link MapKey} annotation type.
   */
  public static ClassName getMapKeyCreatorClassName(TypeElement mapKeyType) {
    ClassName mapKeyTypeName = ClassName.fromTypeElement(mapKeyType);
    return mapKeyTypeName.topLevelClassName().peerNamed(mapKeyTypeName.classFileName() + "Creator");
  }

  /**
   * Returns a snippet for the map key specified by the {@link MapKey} annotation on
   * {@code bindingElement}.
   *
   * @throws IllegalArgumentException if the element is annotated with more than one {@code MapKey}
   *     annotation
   * @throws IllegalStateException if {@code bindingElement} is not annotated with a {@code MapKey}
   *     annotation
   */
  static Snippet getMapKeySnippet(Element bindingElement) {
    AnnotationMirror mapKey = getMapKey(bindingElement).get();
    ClassName mapKeyCreator =
        getMapKeyCreatorClassName(MoreTypes.asTypeElement(mapKey.getAnnotationType()));
    Optional<? extends AnnotationValue> unwrappedValue = unwrapValue(mapKey);
    if (unwrappedValue.isPresent()) {
      return new MapKeySnippetExceptArrays(mapKeyCreator)
          .visit(unwrappedValue.get(), unwrappedValue.get());
    } else {
      return annotationSnippet(mapKey, new MapKeySnippet(mapKeyCreator));
    }
  }

  /**
   * Returns a snippet to create the visited value in code. Expects its parameter to be a class with
   * static creation methods for all nested annotation types.
   *
   * <p>Note that {@link AnnotationValue#toString()} is the source-code representation of the value
   * <em>when used in an annotation</em>, which is not always the same as the representation needed
   * when creating the value in a method body.
   *
   * <p>For example, inside an annotation, a nested array of {@code int}s is simply
   * <code>{1, 2, 3}</code>, but in code it would have to be <code> new int[] {1, 2, 3}</code>.
   */
  private static class MapKeySnippet
      extends SimpleAnnotationValueVisitor6<Snippet, AnnotationValue> {

    final ClassName mapKeyCreator;

    MapKeySnippet(ClassName mapKeyCreator) {
      this.mapKeyCreator = mapKeyCreator;
    }

    @Override
    public Snippet visitEnumConstant(VariableElement c, AnnotationValue p) {
      return Snippet.format(
          "%s.%s", TypeNames.forTypeMirror(c.getEnclosingElement().asType()), c.getSimpleName());
    }

    @Override
    public Snippet visitAnnotation(AnnotationMirror a, AnnotationValue p) {
      return annotationSnippet(a, this);
    }

    @Override
    public Snippet visitType(TypeMirror t, AnnotationValue p) {
      return Snippet.format("%s.class", TypeNames.forTypeMirror(t));
    }

    @Override
    public Snippet visitString(String s, AnnotationValue p) {
      return Snippet.format("%s", p);
    }

    @Override
    public Snippet visitByte(byte b, AnnotationValue p) {
      return Snippet.format("(byte) %s", b);
    }

    @Override
    public Snippet visitChar(char c, AnnotationValue p) {
      return Snippet.format("%s", p);
    }

    @Override
    public Snippet visitDouble(double d, AnnotationValue p) {
      return Snippet.format("%sD", d);
    }

    @Override
    public Snippet visitFloat(float f, AnnotationValue p) {
      return Snippet.format("%sF", f);
    }

    @Override
    public Snippet visitInt(int i, AnnotationValue p) {
      return Snippet.format("(int) %s", i);
    }

    @Override
    public Snippet visitLong(long i, AnnotationValue p) {
      return Snippet.format("%sL", i);
    }

    @Override
    public Snippet visitShort(short s, AnnotationValue p) {
      return Snippet.format("(short) %s", s);
    }

    @Override
    protected Snippet defaultAction(Object o, AnnotationValue p) {
      return Snippet.format("%s", o);
    }

    @Override
    public Snippet visitArray(List<? extends AnnotationValue> values, AnnotationValue p) {
      ImmutableList.Builder<Snippet> snippets = ImmutableList.builder();
      for (int i = 0; i < values.size(); i++) {
        snippets.add(this.visit(values.get(i), p));
      }
      return Snippet.format("{%s}", makeParametersSnippet(snippets.build()));
    }
  }

  /**
   * Returns a snippet for the visited value. Expects its parameter to be a class with static
   * creation methods for all nested annotation types.
   *
   * <p>Throws {@link IllegalArgumentException} if the visited value is an array.
   */
  private static class MapKeySnippetExceptArrays extends MapKeySnippet {

    MapKeySnippetExceptArrays(ClassName mapKeyCreator) {
      super(mapKeyCreator);
    }

    @Override
    public Snippet visitArray(List<? extends AnnotationValue> values, AnnotationValue p) {
      throw new IllegalArgumentException("Cannot unwrap arrays");
    }
  }

  /**
   * Returns a snippet that calls a static method on {@code mapKeySnippet.mapKeyCreator} to create
   * an annotation from {@code mapKeyAnnotation}.
   */
  private static Snippet annotationSnippet(
      AnnotationMirror mapKeyAnnotation, final MapKeySnippet mapKeySnippet) {
    return Snippet.format(
        "%s.create%s(%s)",
        mapKeySnippet.mapKeyCreator,
        mapKeyAnnotation.getAnnotationType().asElement().getSimpleName(),
        makeParametersSnippet(
            transform(
                getAnnotationValuesWithDefaults(mapKeyAnnotation).entrySet(),
                new Function<Map.Entry<ExecutableElement, AnnotationValue>, Snippet>() {
                  @Override
                  public Snippet apply(Map.Entry<ExecutableElement, AnnotationValue> entry) {
                    return ARRAY_LITERAL_PREFIX.visit(
                        entry.getKey().getReturnType(),
                        mapKeySnippet.visit(entry.getValue(), entry.getValue()));
                  }
                })));
  }

  /**
   * If the visited type is an array, prefixes the parameter snippet with {@code new T[]}, where
   * {@code T} is the raw array component type.
   */
  private static final SimpleTypeVisitor6<Snippet, Snippet> ARRAY_LITERAL_PREFIX =
      new SimpleTypeVisitor6<Snippet, Snippet>() {

        @Override
        public Snippet visitArray(ArrayType t, Snippet p) {
          return Snippet.format("new %s[] %s", RAW_TYPE_NAME.visit(t.getComponentType()), p);
        }

        @Override
        protected Snippet defaultAction(TypeMirror e, Snippet p) {
          return p;
        }
      };

  /**
   * If the visited type is an array, returns the name of its raw component type; otherwise returns
   * the name of the type itself.
   */
  private static final SimpleTypeVisitor6<TypeName, Void> RAW_TYPE_NAME =
      new SimpleTypeVisitor6<TypeName, Void>() {
        @Override
        public TypeName visitDeclared(DeclaredType t, Void p) {
          return ClassName.fromTypeElement(MoreTypes.asTypeElement(t));
        }

        @Override
        protected TypeName defaultAction(TypeMirror e, Void p) {
          return TypeNames.forTypeMirror(e);
        }
      };

  private MapKeys() {}
}