/*
 * Copyright (C) 2016 The Dagger Authors.
 *
 * 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.binding;

import static com.google.auto.common.AnnotationMirrors.getAnnotationValuesWithDefaults;
import static dagger.internal.codegen.binding.SourceFiles.classFileName;
import static dagger.internal.codegen.javapoet.CodeBlocks.makeParametersCodeBlock;
import static java.util.stream.Collectors.toList;

import com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
import com.google.common.collect.ImmutableList;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.TypeName;
import java.util.List;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
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.TypeMirror;
import javax.lang.model.util.SimpleAnnotationValueVisitor6;
import javax.lang.model.util.SimpleTypeVisitor6;

/**
 * Returns an expression creating an instance of the visited annotation type. Its parameter must be
 * a class as generated by {@link dagger.internal.codegen.writing.AnnotationCreatorGenerator}.
 *
 * <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}},
 * but in code it would have to be {@code new int[] {1, 2, 3}}.
 */
public class AnnotationExpression
    extends SimpleAnnotationValueVisitor6<CodeBlock, AnnotationValue> {

  private final AnnotationMirror annotation;
  private final ClassName creatorClass;

  AnnotationExpression(AnnotationMirror annotation) {
    this.annotation = annotation;
    this.creatorClass =
        getAnnotationCreatorClassName(
            MoreTypes.asTypeElement(annotation.getAnnotationType()));
  }

  /**
   * Returns an expression that calls static methods on the annotation's creator class to create an
   * annotation instance equivalent the annotation passed to the constructor.
   */
  CodeBlock getAnnotationInstanceExpression() {
    return getAnnotationInstanceExpression(annotation);
  }

  private CodeBlock getAnnotationInstanceExpression(AnnotationMirror annotation) {
    return CodeBlock.of(
        "$T.$L($L)",
        creatorClass,
        createMethodName(
            MoreElements.asType(annotation.getAnnotationType().asElement())),
        makeParametersCodeBlock(
            getAnnotationValuesWithDefaults(annotation)
                .entrySet()
                .stream()
                .map(entry -> getValueExpression(entry.getKey().getReturnType(), entry.getValue()))
                .collect(toList())));
  }

  /**
   * Returns the name of the generated class that contains the static {@code create} methods for an
   * annotation type.
   */
  public static ClassName getAnnotationCreatorClassName(TypeElement annotationType) {
    ClassName annotationTypeName = ClassName.get(annotationType);
    return annotationTypeName
        .topLevelClassName()
        .peerClass(classFileName(annotationTypeName) + "Creator");
  }

  public static String createMethodName(TypeElement annotationType) {
    return "create" + annotationType.getSimpleName();
  }

  /**
   * Returns an expression that evaluates to a {@code value} of a given type on an {@code
   * annotation}.
   */
  CodeBlock getValueExpression(TypeMirror valueType, AnnotationValue value) {
    return ARRAY_LITERAL_PREFIX.visit(valueType, this.visit(value, value));
  }

  @Override
  public CodeBlock visitEnumConstant(VariableElement c, AnnotationValue p) {
    return CodeBlock.of("$T.$L", c.getEnclosingElement(), c.getSimpleName());
  }

  @Override
  public CodeBlock visitAnnotation(AnnotationMirror a, AnnotationValue p) {
    return getAnnotationInstanceExpression(a);
  }

  @Override
  public CodeBlock visitType(TypeMirror t, AnnotationValue p) {
    return CodeBlock.of("$T.class", t);
  }

  @Override
  public CodeBlock visitString(String s, AnnotationValue p) {
    return CodeBlock.of("$S", s);
  }

  @Override
  public CodeBlock visitByte(byte b, AnnotationValue p) {
    return CodeBlock.of("(byte) $L", b);
  }

  @Override
  public CodeBlock visitChar(char c, AnnotationValue p) {
    return CodeBlock.of("$L", p);
  }

  @Override
  public CodeBlock visitDouble(double d, AnnotationValue p) {
    return CodeBlock.of("$LD", d);
  }

  @Override
  public CodeBlock visitFloat(float f, AnnotationValue p) {
    return CodeBlock.of("$LF", f);
  }

  @Override
  public CodeBlock visitLong(long i, AnnotationValue p) {
    return CodeBlock.of("$LL", i);
  }

  @Override
  public CodeBlock visitShort(short s, AnnotationValue p) {
    return CodeBlock.of("(short) $L", s);
  }

  @Override
  protected CodeBlock defaultAction(Object o, AnnotationValue p) {
    return CodeBlock.of("$L", o);
  }

  @Override
  public CodeBlock visitArray(List<? extends AnnotationValue> values, AnnotationValue p) {
    ImmutableList.Builder<CodeBlock> codeBlocks = ImmutableList.builder();
    for (AnnotationValue value : values) {
      codeBlocks.add(this.visit(value, p));
    }
    return CodeBlock.of("{$L}", makeParametersCodeBlock(codeBlocks.build()));
  }

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

        @Override
        public CodeBlock visitArray(ArrayType t, CodeBlock p) {
          return CodeBlock.of("new $T[] $L", RAW_TYPE_NAME.visit(t.getComponentType()), p);
        }

        @Override
        protected CodeBlock defaultAction(TypeMirror e, CodeBlock 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.get(MoreTypes.asTypeElement(t));
        }

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