/*
 * Copyright 2021 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.common.collect.Sets.difference;

import com.google.auto.common.MoreElements;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import java.util.Optional;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;

class BuilderMethodClassifierForAutoValue extends BuilderMethodClassifier<ExecutableElement> {
  private final ErrorReporter errorReporter;
  private final ImmutableBiMap<ExecutableElement, String> getterToPropertyName;
  private final ImmutableMap<String, ExecutableElement> getterNameToGetter;
  private final TypeMirror builtType;

  private BuilderMethodClassifierForAutoValue(
      ErrorReporter errorReporter,
      ProcessingEnvironment processingEnv,
      TypeMirror builtType,
      TypeElement builderType,
      ImmutableBiMap<ExecutableElement, String> getterToPropertyName,
      ImmutableMap<String, TypeMirror> rewrittenPropertyTypes) {
    super(errorReporter, processingEnv, builtType, builderType, rewrittenPropertyTypes);
    this.errorReporter = errorReporter;
    this.getterToPropertyName = getterToPropertyName;
    this.getterNameToGetter =
        Maps.uniqueIndex(getterToPropertyName.keySet(), m -> m.getSimpleName().toString());
    this.builtType = builtType;
  }

  /**
   * Classifies the given methods from a builder type and its ancestors.
   *
   * @param methods the abstract methods in {@code builderType} and its ancestors.
   * @param errorReporter where to report errors.
   * @param processingEnv the {@link ProcessingEnvironment} for annotation processing.
   * @param autoValueClass the {@code AutoValue} class containing the builder.
   * @param builderType the builder class or interface within {@code autoValueClass}.
   * @param getterToPropertyName a map from getter methods to the properties they get.
   * @param rewrittenPropertyTypes a map from property names to types. The types here use type
   *     parameters from the builder class (if any) rather than from the {@code AutoValue} class,
   *     even though the getter methods are in the latter.
   * @param autoValueHasToBuilder true if the containing {@code @AutoValue} class has a {@code
   *     toBuilder()} method.
   * @return an {@code Optional} that contains the results of the classification if it was
   *     successful or nothing if it was not.
   */
  static Optional<BuilderMethodClassifier<ExecutableElement>> classify(
      Iterable<ExecutableElement> methods,
      ErrorReporter errorReporter,
      ProcessingEnvironment processingEnv,
      TypeElement autoValueClass,
      TypeElement builderType,
      ImmutableBiMap<ExecutableElement, String> getterToPropertyName,
      ImmutableMap<String, TypeMirror> rewrittenPropertyTypes,
      boolean autoValueHasToBuilder) {
    BuilderMethodClassifier<ExecutableElement> classifier =
        new BuilderMethodClassifierForAutoValue(
            errorReporter,
            processingEnv,
            autoValueClass.asType(),
            builderType,
            getterToPropertyName,
            rewrittenPropertyTypes);
    if (classifier.classifyMethods(methods, autoValueHasToBuilder)) {
      return Optional.of(classifier);
    } else {
      return Optional.empty();
    }
  }

  @Override
  TypeMirror originalPropertyType(ExecutableElement propertyElement) {
    return propertyElement.getReturnType();
  }

  @Override
  String propertyString(ExecutableElement propertyElement) {
    TypeElement type = MoreElements.asType(propertyElement.getEnclosingElement());
    return "property method "
        + type.getQualifiedName()
        + "."
        + propertyElement.getSimpleName()
        + "()";
  }

  @Override
  ImmutableBiMap<String, ExecutableElement> propertyElements() {
    return getterToPropertyName.inverse();
  }

  @Override
  Optional<String> propertyForBuilderGetter(ExecutableElement method) {
    String methodName = method.getSimpleName().toString();
    return Optional.ofNullable(getterNameToGetter.get(methodName)).map(getterToPropertyName::get);
  }

  @Override
  void checkForFailedJavaBean(ExecutableElement rejectedSetter) {
    ImmutableSet<ExecutableElement> allGetters = getterToPropertyName.keySet();
    ImmutableSet<ExecutableElement> prefixedGetters =
        AutoValueProcessor.prefixedGettersIn(allGetters);
    if (prefixedGetters.size() < allGetters.size()
        && prefixedGetters.size() >= allGetters.size() / 2) {
      errorReporter.reportNote(
          rejectedSetter,
          "This might be because you are using the getFoo() convention"
              + " for some but not all methods. These methods don't follow the convention: %s",
          difference(allGetters, prefixedGetters));
    }
  }

  @Override
  String autoWhat() {
    return "AutoValue";
  }

  @Override
  String getterMustMatch() {
    return "a property method of " + builtType;
  }

  @Override
  String fooBuilderMustMatch() {
    return "foo() or getFoo()";
  }
}
