/*
 * Copyright 2014 Google LLC
 *
 * 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.google.auto.value.processor;

import static com.google.auto.common.MoreElements.getLocalAndInheritedMethods;
import static com.google.auto.value.processor.AutoValueOrOneOfProcessor.hasAnnotationMirror;
import static com.google.auto.value.processor.AutoValueOrOneOfProcessor.nullableAnnotationFor;
import static com.google.auto.value.processor.ClassNames.AUTO_VALUE_BUILDER_NAME;
import static com.google.common.collect.Sets.immutableEnumSet;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static javax.lang.model.util.ElementFilter.methodsIn;
import static javax.lang.model.util.ElementFilter.typesIn;

import com.google.auto.common.MoreTypes;
import com.google.auto.value.extension.AutoValueExtension;
import com.google.auto.value.processor.AutoValueOrOneOfProcessor.Property;
import com.google.auto.value.processor.PropertyBuilderClassifier.PropertyBuilder;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;

/**
 * Support for AutoValue builders.
 *
 * @author √Čamonn McManus
 */
class BuilderSpec {
  private final TypeElement autoValueClass;
  private final ProcessingEnvironment processingEnv;
  private final ErrorReporter errorReporter;

  BuilderSpec(
      TypeElement autoValueClass,
      ProcessingEnvironment processingEnv,
      ErrorReporter errorReporter) {
    this.autoValueClass = autoValueClass;
    this.processingEnv = processingEnv;
    this.errorReporter = errorReporter;
  }

  private static final ImmutableSet<ElementKind> CLASS_OR_INTERFACE =
      immutableEnumSet(ElementKind.CLASS, ElementKind.INTERFACE);

  /**
   * Determines if the {@code @AutoValue} class for this instance has a correct nested
   * {@code @AutoValue.Builder} class or interface and return a representation of it in an {@code
   * Optional} if so.
   */
  Optional<Builder> getBuilder() {
    Optional<TypeElement> builderTypeElement = Optional.empty();
    for (TypeElement containedClass : typesIn(autoValueClass.getEnclosedElements())) {
      if (hasAnnotationMirror(containedClass, AUTO_VALUE_BUILDER_NAME)) {
        if (!CLASS_OR_INTERFACE.contains(containedClass.getKind())) {
          errorReporter.reportError(
              containedClass, "@AutoValue.Builder can only apply to a class or an interface");
        } else if (!containedClass.getModifiers().contains(Modifier.STATIC)) {
          errorReporter.reportError(
              containedClass, "@AutoValue.Builder cannot be applied to a non-static class");
        } else if (builderTypeElement.isPresent()) {
          errorReporter.reportError(
              containedClass,
              "%s already has a Builder: %s",
              autoValueClass,
              builderTypeElement.get());
        } else {
          builderTypeElement = Optional.of(containedClass);
        }
      }
    }

    if (builderTypeElement.isPresent()) {
      return builderFrom(builderTypeElement.get());
    } else {
      return Optional.empty();
    }
  }

  /** Representation of an {@code AutoValue.Builder} class or interface. */
  class Builder implements AutoValueExtension.BuilderContext {
    private final TypeElement builderTypeElement;
    private ImmutableSet<ExecutableElement> toBuilderMethods;
    private ExecutableElement buildMethod;
    private BuilderMethodClassifier classifier;

    Builder(TypeElement builderTypeElement) {
      this.builderTypeElement = builderTypeElement;
    }

    @Override
    public TypeElement builderType() {
      return builderTypeElement;
    }

    @Override
    public Set<ExecutableElement> builderMethods() {
      return methodsIn(autoValueClass.getEnclosedElements()).stream()
          .filter(
              m ->
                  m.getParameters().isEmpty()
                      && m.getModifiers().contains(Modifier.STATIC)
                      && !m.getModifiers().contains(Modifier.PRIVATE)
                      && erasedTypeIs(m.getReturnType(), builderTypeElement))
          .collect(toSet());
    }

    @Override
    public Optional<ExecutableElement> buildMethod() {
      return methodsIn(builderTypeElement.getEnclosedElements()).stream()
          .filter(
              m ->
                  m.getSimpleName().contentEquals("build")
                      && !m.getModifiers().contains(Modifier.PRIVATE)
                      && !m.getModifiers().contains(Modifier.STATIC)
                      && m.getParameters().isEmpty()
                      && erasedTypeIs(m.getReturnType(), autoValueClass))
          .findFirst();
    }

    @Override
    public ExecutableElement autoBuildMethod() {
      return buildMethod;
    }

    @Override
    public Map<String, Set<ExecutableElement>> setters() {
      return Maps.transformValues(
          classifier.propertyNameToSetters().asMap(),
          propertySetters ->
              propertySetters.stream().map(PropertySetter::getSetter).collect(toSet()));
    }

    @Override
    public Map<String, ExecutableElement> propertyBuilders() {
      return Maps.transformValues(
          classifier.propertyNameToPropertyBuilder(), PropertyBuilder::getPropertyBuilderMethod);
    }

    private boolean erasedTypeIs(TypeMirror type, TypeElement baseType) {
      return type.getKind().equals(TypeKind.DECLARED)
          && MoreTypes.asDeclared(type).asElement().equals(baseType);
    }

    @Override
    public Set<ExecutableElement> toBuilderMethods() {
      return toBuilderMethods;
    }

    /**
     * Finds any methods in the set that return the builder type. If the builder has type parameters
     * {@code <A, B>}, then the return type of the method must be {@code Builder<A, B>} with the
     * same parameter names. We enforce elsewhere that the names and bounds of the builder
     * parameters must be the same as those of the @AutoValue class. Here's a correct example:
     *
     * <pre>
     * {@code @AutoValue abstract class Foo<A extends Number, B> {
     *   abstract int someProperty();
     *
     *   abstract Builder<A, B> toBuilder();
     *
     *   interface Builder<A extends Number, B> {...}
     * }}
     * </pre>
     *
     * <p>We currently impose that there cannot be more than one such method.
     */
    ImmutableSet<ExecutableElement> toBuilderMethods(
        Types typeUtils, Set<ExecutableElement> abstractMethods) {

      List<String> builderTypeParamNames =
          builderTypeElement.getTypeParameters().stream()
              .map(e -> e.getSimpleName().toString())
              .collect(toList());

      ImmutableSet.Builder<ExecutableElement> methods = ImmutableSet.builder();
      for (ExecutableElement method : abstractMethods) {
        if (builderTypeElement.equals(typeUtils.asElement(method.getReturnType()))) {
          methods.add(method);
          DeclaredType returnType = MoreTypes.asDeclared(method.getReturnType());
          List<String> typeArguments =
              returnType.getTypeArguments().stream()
                  .filter(t -> t.getKind().equals(TypeKind.TYPEVAR))
                  .map(t -> typeUtils.asElement(t).getSimpleName().toString())
                  .collect(toList());
          if (!builderTypeParamNames.equals(typeArguments)) {
            errorReporter.reportError(
                method,
                "Builder converter method should return %s%s",
                builderTypeElement,
                TypeSimplifier.actualTypeParametersString(builderTypeElement));
          }
        }
      }
      ImmutableSet<ExecutableElement> builderMethods = methods.build();
      if (builderMethods.size() > 1) {
        errorReporter.reportError(
            builderMethods.iterator().next(), "There can be at most one builder converter method");
      }
      this.toBuilderMethods = builderMethods;
      return builderMethods;
    }

    void defineVars(
        AutoValueTemplateVars vars,
        ImmutableBiMap<ExecutableElement, String> getterToPropertyName) {
      Iterable<ExecutableElement> builderMethods = abstractMethods(builderTypeElement);
      boolean autoValueHasToBuilder = !toBuilderMethods.isEmpty();
      ImmutableMap<ExecutableElement, TypeMirror> getterToPropertyType =
          TypeVariables.rewriteReturnTypes(
              processingEnv.getElementUtils(),
              processingEnv.getTypeUtils(),
              getterToPropertyName.keySet(),
              autoValueClass,
              builderTypeElement);
      Optional<BuilderMethodClassifier> optionalClassifier =
          BuilderMethodClassifier.classify(
              builderMethods,
              errorReporter,
              processingEnv,
              autoValueClass,
              builderTypeElement,
              getterToPropertyName,
              getterToPropertyType,
              autoValueHasToBuilder);
      if (!optionalClassifier.isPresent()) {
        return;
      }
      for (ExecutableElement method : methodsIn(builderTypeElement.getEnclosedElements())) {
        if (method.getSimpleName().contentEquals("builder")
                && method.getModifiers().contains(Modifier.STATIC)
                && method.getAnnotationMirrors().isEmpty()) {
          // For now we ignore methods with annotations, because for example we do want to allow
          // Jackson's @JsonCreator.
          errorReporter.reportWarning(
              method, "Static builder() method should be in the containing class");
        }
      }
      this.classifier = optionalClassifier.get();
      Set<ExecutableElement> buildMethods = classifier.buildMethods();
      if (buildMethods.size() != 1) {
        Set<? extends Element> errorElements =
            buildMethods.isEmpty() ? ImmutableSet.of(builderTypeElement) : buildMethods;
        for (Element buildMethod : errorElements) {
          errorReporter.reportError(
              buildMethod,
              "Builder must have a single no-argument method returning %s%s",
              autoValueClass,
              typeParamsString());
        }
        return;
      }
      this.buildMethod = Iterables.getOnlyElement(buildMethods);
      vars.builderIsInterface = builderTypeElement.getKind() == ElementKind.INTERFACE;
      vars.builderTypeName = TypeSimplifier.classNameOf(builderTypeElement);
      vars.builderFormalTypes = TypeEncoder.formalTypeParametersString(builderTypeElement);
      vars.builderActualTypes = TypeSimplifier.actualTypeParametersString(builderTypeElement);
      vars.buildMethod = Optional.of(new SimpleMethod(buildMethod));
      vars.builderGetters = classifier.builderGetters();
      vars.builderSetters = classifier.propertyNameToSetters();

      vars.builderPropertyBuilders =
          ImmutableMap.copyOf(classifier.propertyNameToPropertyBuilder());

      Set<Property> required = new LinkedHashSet<>(vars.props);
      for (Property property : vars.props) {
        if (property.isNullable()
            || property.getOptional() != null
            || vars.builderPropertyBuilders.containsKey(property.getName())) {
          required.remove(property);
        }
      }
      vars.builderRequiredProperties = ImmutableSet.copyOf(required);
    }
  }

  /**
   * Information about a builder property getter, referenced from the autovalue.vm template. A
   * property called foo (defined by a method {@code T foo()} or {@code T getFoo()}) can have a
   * getter method in the builder with the same name ({@code foo()} or {@code getFoo()}) and a
   * return type of either {@code T} or {@code Optional<T>}. The {@code Optional<T>} form can be
   * used to tell whether the property has been set. Here, {@code Optional<T>} can be either {@code
   * java.util.Optional} or {@code com.google.common.base.Optional}. If {@code T} is {@code int},
   * {@code long}, or {@code double}, then instead of {@code Optional<T>} we can have {@code
   * OptionalInt} etc. If {@code T} is a primitive type (including these ones but also the other
   * five) then {@code Optional<T>} can be the corresponding boxed type.
   */
  public static class PropertyGetter {
    private final String access;
    private final String type;
    private final Optionalish optional;

    /**
     * Makes a new {@code PropertyGetter} instance.
     *
     * @param method the source method which this getter is implementing.
     * @param type the type that the getter returns. This is written to take imports into account,
     *     so it might be {@code List<String>} for example. It is either identical to the type of
     *     the corresponding getter in the {@code @AutoValue} class, or it is an optional wrapper,
     *     like {@code Optional<List<String>>}.
     * @param optional a representation of the {@code Optional} type that the getter returns, if
     *     this is an optional getter, or null otherwise. An optional getter is one that returns
     *     {@code Optional<T>} rather than {@code T}, as explained above.
     */
    PropertyGetter(ExecutableElement method, String type, Optionalish optional) {
      this.access = SimpleMethod.access(method);
      this.type = type;
      this.optional = optional;
    }

    public String getAccess() {
      return access;
    }

    public String getType() {
      return type;
    }

    public Optionalish getOptional() {
      return optional;
    }
  }

  /**
   * Information about a property setter, referenced from the autovalue.vm template. A property
   * called foo (defined by a method {@code T foo()} or {@code T getFoo()}) can have a setter method
   * {@code foo(T)} or {@code setFoo(T)} that returns the builder type. Additionally, it can have a
   * setter with a type that can be copied to {@code T} through a {@code copyOf} method; for example
   * a property {@code foo} of type {@code ImmutableSet<String>} can be set with a method {@code
   * setFoo(Collection<String> foos)}. And, if {@code T} is {@code Optional}, it can have a setter
   * with a type that can be copied to {@code T} through {@code Optional.of}.
   */
  public static class PropertySetter {
    private final ExecutableElement setter;
    private final String access;
    private final String name;
    private final String parameterTypeString;
    private final boolean primitiveParameter;
    private final String nullableAnnotation;
    private final Function<String, String> copyFunction;

    PropertySetter(
        ExecutableElement setter, TypeMirror parameterType, Function<String, String> copyFunction) {
      this.setter = setter;
      this.copyFunction = copyFunction;
      this.access = SimpleMethod.access(setter);
      this.name = setter.getSimpleName().toString();
      primitiveParameter = parameterType.getKind().isPrimitive();
      this.parameterTypeString = parameterTypeString(setter, parameterType);
      VariableElement parameterElement = Iterables.getOnlyElement(setter.getParameters());
      Optional<String> maybeNullable = nullableAnnotationFor(parameterElement, parameterType);
      this.nullableAnnotation = maybeNullable.orElse("");
    }

    ExecutableElement getSetter() {
      return setter;
    }

    private static String parameterTypeString(ExecutableElement setter, TypeMirror parameterType) {
      if (setter.isVarArgs()) {
        TypeMirror componentType = MoreTypes.asArray(parameterType).getComponentType();
        // This is a bit ugly. It's OK to annotate just the component type, because if it is
        // say `@Nullable String` then we will end up with `@Nullable String...`. Unlike the
        // normal array case, we can't have the situation where the array itself is annotated;
        // you can write `String @Nullable []` to mean that, but you can't write
        // `String @Nullable ...`.
        return TypeEncoder.encodeWithAnnotations(componentType) + "...";
      } else {
        return TypeEncoder.encodeWithAnnotations(parameterType);
      }
    }

    public String getAccess() {
      return access;
    }

    public String getName() {
      return name;
    }

    public String getParameterType() {
      return parameterTypeString;
    }

    public boolean getPrimitiveParameter() {
      return primitiveParameter;
    }

    public String getNullableAnnotation() {
      return nullableAnnotation;
    }

    public String copy(AutoValueProcessor.Property property) {
      String copy = copyFunction.apply(property.toString());

      // Add a null guard only in cases where we are using copyOf and the property is @Nullable.
      if (property.isNullable() && !copy.equals(property.toString())) {
        copy = String.format("(%s == null ? null : %s)", property, copy);
      }

      return copy;
    }
  }

  /**
   * Returns a representation of the given {@code @AutoValue.Builder} class or interface. If the
   * class or interface has abstract methods that could not be part of any builder, emits error
   * messages and returns Optional.empty().
   */
  private Optional<Builder> builderFrom(TypeElement builderTypeElement) {

    // We require the builder to have the same type parameters as the @AutoValue class, meaning the
    // same names and bounds. In principle the type parameters could have different names, but that
    // would be confusing, and our code would reject it anyway because it wouldn't consider that
    // the return type of Foo<U> build() was really the same as the declaration of Foo<T>. This
    // check produces a better error message in that case and similar ones.

    if (!sameTypeParameters(autoValueClass, builderTypeElement)) {
      errorReporter.reportError(
          builderTypeElement,
          "Type parameters of %s must have same names and bounds as type parameters of %s",
          builderTypeElement,
          autoValueClass);
      return Optional.empty();
    }
    return Optional.of(new Builder(builderTypeElement));
  }

  private static boolean sameTypeParameters(TypeElement a, TypeElement b) {
    int nTypeParameters = a.getTypeParameters().size();
    if (nTypeParameters != b.getTypeParameters().size()) {
      return false;
    }
    for (int i = 0; i < nTypeParameters; i++) {
      TypeParameterElement aParam = a.getTypeParameters().get(i);
      TypeParameterElement bParam = b.getTypeParameters().get(i);
      if (!aParam.getSimpleName().equals(bParam.getSimpleName())) {
        return false;
      }
      Set<TypeMirror> autoValueBounds = new TypeMirrorSet(aParam.getBounds());
      Set<TypeMirror> builderBounds = new TypeMirrorSet(bParam.getBounds());
      if (!autoValueBounds.equals(builderBounds)) {
        return false;
      }
    }
    return true;
  }

  /**
   * Returns a set of all abstract methods in the given TypeElement or inherited from ancestors. If
   * any of the abstract methods has a return type or parameter type that is not currently defined
   * then this method will throw an exception that will cause us to defer processing of the current
   * class until a later annotation-processing round.
   */
  private ImmutableSet<ExecutableElement> abstractMethods(TypeElement typeElement) {
    Set<ExecutableElement> methods =
        getLocalAndInheritedMethods(
            typeElement, processingEnv.getTypeUtils(), processingEnv.getElementUtils());
    ImmutableSet.Builder<ExecutableElement> abstractMethods = ImmutableSet.builder();
    for (ExecutableElement method : methods) {
      if (method.getModifiers().contains(Modifier.ABSTRACT)) {
        MissingTypes.deferIfMissingTypesIn(method);
        abstractMethods.add(method);
      }
    }
    return abstractMethods.build();
  }

  private String typeParamsString() {
    return TypeSimplifier.actualTypeParametersString(autoValueClass);
  }
}