/*
 * Copyright (C) 2014 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 androidx.room.compiler.processing.XElementKt.isConstructor;
import static androidx.room.compiler.processing.XElementKt.isField;
import static androidx.room.compiler.processing.XElementKt.isMethod;
import static androidx.room.compiler.processing.XElementKt.isMethodParameter;
import static androidx.room.compiler.processing.XElementKt.isTypeElement;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Iterables.getOnlyElement;
import static dagger.internal.codegen.binding.SourceFiles.factoryNameForElement;
import static dagger.internal.codegen.binding.SourceFiles.memberInjectedFieldSignatureForVariable;
import static dagger.internal.codegen.binding.SourceFiles.membersInjectorNameForType;
import static dagger.internal.codegen.extension.DaggerCollectors.toOptional;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet;
import static dagger.internal.codegen.xprocessing.XElements.asField;
import static dagger.internal.codegen.xprocessing.XElements.asMethod;
import static dagger.internal.codegen.xprocessing.XElements.asMethodParameter;
import static dagger.internal.codegen.xprocessing.XElements.asTypeElement;
import static dagger.internal.codegen.xprocessing.XElements.closestEnclosingTypeElement;

import androidx.room.compiler.processing.XAnnotation;
import androidx.room.compiler.processing.XConstructorElement;
import androidx.room.compiler.processing.XElement;
import androidx.room.compiler.processing.XExecutableElement;
import androidx.room.compiler.processing.XFieldElement;
import androidx.room.compiler.processing.XProcessingEnv;
import androidx.room.compiler.processing.XTypeElement;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.squareup.javapoet.ClassName;
import dagger.internal.codegen.base.DaggerSuperficialValidation;
import dagger.internal.codegen.base.ElementFormatter;
import dagger.internal.codegen.compileroption.CompilerOptions;
import dagger.internal.codegen.javapoet.TypeNames;
import dagger.internal.codegen.kotlin.KotlinMetadataUtil;
import dagger.internal.codegen.model.DaggerAnnotation;
import dagger.internal.codegen.model.Scope;
import dagger.internal.codegen.xprocessing.XAnnotations;
import java.util.Optional;
import java.util.stream.Stream;
import javax.inject.Inject;

/** Utilities relating to annotations defined in the {@code javax.inject} package. */
public final class InjectionAnnotations {
  private final XProcessingEnv processingEnv;
  private final KotlinMetadataUtil kotlinMetadataUtil;
  private final DaggerSuperficialValidation superficialValidation;
  private final CompilerOptions compilerOptions;

  @Inject
  InjectionAnnotations(
      XProcessingEnv processingEnv,
      KotlinMetadataUtil kotlinMetadataUtil,
      DaggerSuperficialValidation superficialValidation,
      CompilerOptions compilerOptions) {
    this.processingEnv = processingEnv;
    this.kotlinMetadataUtil = kotlinMetadataUtil;
    this.superficialValidation = superficialValidation;
    this.compilerOptions = compilerOptions;
  }

  /**
   * Returns the scope on the given element if it exists.
   *
   * <p>The {@code ScopeMetadata} is used to avoid superficial validation on unnecessary
   * annotations. If the {@code ScopeMetadata} does not exist, then all annotations must be
   * superficially validated before we can determine if they are scopes or not.
   *
   * @throws IllegalArgumentException if the given element has more than one scope.
   */
  public Optional<Scope> getScope(XElement element) {
    return getScopes(element).stream().collect(toOptional());
  }

  /**
   * Returns the scopes on the given element, or an empty set if none exist.
   *
   * <p>Note: Use {@link #getScope(XElement)} if the usage of the scope on the given element has
   * already been validated and known to be unique. This method should typically only be used in the
   * process of such validation.
   *
   * <p>The {@code ScopeMetadata} is used to avoid superficial validation on unnecessary
   * annotations. If the {@code ScopeMetadata} does not exist, then all annotations must be
   * superficially validated before we can determine if they are scopes or not.
   */
  public ImmutableSet<Scope> getScopes(XElement element) {
    superficialValidation.validateTypeOf(element);
    ImmutableSet<Scope> scopes =
        getScopesWithMetadata(element).orElseGet(() -> getScopesWithoutMetadata(element));

    // Fully validate each scope to ensure its values are also valid.
    scopes.stream()
        .map(scope -> scope.scopeAnnotation().xprocessing())
        .forEach(scope -> superficialValidation.validateAnnotationOf(element, scope));
    return scopes;
  }

  private ImmutableSet<Scope> getScopesWithoutMetadata(XElement element) {
    // Validate the annotation types before we check for @Scope, otherwise the @Scope
    // annotation may appear to be missing (b/213880825).
    superficialValidation.validateAnnotationTypesOf(element);
    return element.getAllAnnotations().stream()
        .filter(InjectionAnnotations::hasScopeAnnotation)
        .map(DaggerAnnotation::from)
        .map(Scope::scope)
        .collect(toImmutableSet());
  }

  private Optional<ImmutableSet<Scope>> getScopesWithMetadata(XElement element) {
    Optional<XAnnotation> scopeMetadata = getScopeMetadata(element);
    if (!scopeMetadata.isPresent()) {
      return Optional.empty();
    }
    String scopeName = scopeMetadata.get().getAsString("value");
    if (scopeName.isEmpty()) {
      return Optional.of(ImmutableSet.of());
    }
    ImmutableList<XAnnotation> scopeAnnotations =
        element.getAllAnnotations().stream()
            .filter(
                annotation ->
                    scopeName.contentEquals(
                        annotation.getType().getTypeElement().getQualifiedName()))
            .collect(toImmutableList());
    checkState(
        scopeAnnotations.size() == 1,
        "Expected %s to have a scope annotation for %s but found: %s",
        ElementFormatter.elementToString(element),
        scopeName,
        scopeAnnotations.stream().map(XAnnotations::toStableString).collect(toImmutableList()));
    XAnnotation scopeAnnotation = getOnlyElement(scopeAnnotations);
    // Do superficial validation before we convert to a Scope, otherwise the @Scope annotation may
    // appear to be missing from the annotation if it's no longer on the classpath.
    superficialValidation.validateAnnotationTypeOf(element, scopeAnnotation);

    // If strictSuperficialValidation is disabled, then we fall back to the old behavior where
    // we may potentially miss a scope rather than report an exception.
    if (compilerOptions.strictSuperficialValidation()) {
      return Optional.of(ImmutableSet.of(Scope.scope(DaggerAnnotation.from(scopeAnnotation))));
    } else {
      return Scope.isScope(DaggerAnnotation.from(scopeAnnotation))
          ? Optional.of(ImmutableSet.of(Scope.scope(DaggerAnnotation.from(scopeAnnotation))))
          : Optional.empty();
    }
  }

  private Optional<XAnnotation> getScopeMetadata(XElement element) {
    return getGeneratedNameForScopeMetadata(element)
        .flatMap(factoryName -> Optional.ofNullable(processingEnv.findTypeElement(factoryName)))
        .flatMap(factory -> Optional.ofNullable(factory.getAnnotation(TypeNames.SCOPE_METADATA)));
  }

  private Optional<ClassName> getGeneratedNameForScopeMetadata(XElement element) {
    // Currently, we only support ScopeMetadata for inject-constructor types and provides methods.
    if (isTypeElement(element)) {
      return asTypeElement(element).getConstructors().stream()
          .filter(InjectionAnnotations::hasInjectOrAssistedInjectAnnotation)
          .findFirst()
          .map(SourceFiles::factoryNameForElement);
    } else if (isMethod(element) && element.hasAnnotation(TypeNames.PROVIDES)) {
      return Optional.of(factoryNameForElement(asMethod(element)));
    }
    return Optional.empty();
  }

  /**
   * Returns the qualifier on the given element if it exists.
   *
   * <p>The {@code QualifierMetadata} is used to avoid superficial validation on unnecessary
   * annotations. If the {@code QualifierMetadata} does not exist, then all annotations must be
   * superficially validated before we can determine if they are qualifiers or not.
   *
   * @throws IllegalArgumentException if the given element has more than one qualifier.
   */
  public Optional<XAnnotation> getQualifier(XElement element) {
    checkNotNull(element);
    ImmutableSet<XAnnotation> qualifierAnnotations = getQualifiers(element);
    switch (qualifierAnnotations.size()) {
      case 0:
        return Optional.empty();
      case 1:
        return Optional.of(getOnlyElement(qualifierAnnotations));
      default:
        throw new IllegalArgumentException(
            element + " was annotated with more than one @Qualifier annotation");
    }
  }

  /*
   * Returns the qualifiers on the given element, or an empty set if none exist.
   *
   * <p>The {@code QualifierMetadata} is used to avoid superficial validation on unnecessary
   * annotations. If the {@code QualifierMetadata} does not exist, then all annotations must be
   * superficially validated before we can determine if they are qualifiers or not.
   */
  public ImmutableSet<XAnnotation> getQualifiers(XElement element) {
    superficialValidation.validateTypeOf(element);
    ImmutableSet<XAnnotation> qualifiers =
        getQualifiersWithMetadata(element)
            .orElseGet(() -> getQualifiersWithoutMetadata(element));

    if (isField(element)) {
      XFieldElement field = asField(element);
      // static/top-level injected fields are not supported,
      // no need to get qualifier from kotlin metadata
      if (!field.isStatic()
          && isTypeElement(field.getEnclosingElement())
          && hasInjectAnnotation(field)
          && kotlinMetadataUtil.hasMetadata(field)) {
        qualifiers =
            Stream.concat(qualifiers.stream(), getQualifiersForKotlinProperty(field).stream())
                .map(DaggerAnnotation::from) // Wrap in DaggerAnnotation to deduplicate
                .distinct()
                .map(DaggerAnnotation::xprocessing)
                .collect(toImmutableSet());
      }
    }

    // Fully validate each qualifier to ensure its values are also valid.
    qualifiers.forEach(qualifier -> superficialValidation.validateAnnotationOf(element, qualifier));

    return qualifiers;
  }

  private ImmutableSet<XAnnotation> getQualifiersWithoutMetadata(XElement element) {
    // Validate the annotation types before we check for @Qualifier, otherwise the
    // @Qualifier annotation may appear to be missing (b/213880825).
    superficialValidation.validateAnnotationTypesOf(element);
    return element.getAllAnnotations().stream()
        .filter(InjectionAnnotations::hasQualifierAnnotation)
        .collect(toImmutableSet());
  }

  private Optional<ImmutableSet<XAnnotation>> getQualifiersWithMetadata(XElement element) {
    Optional<XAnnotation> qualifierMetadata = getQualifierMetadata(element);
    if (!qualifierMetadata.isPresent()) {
      return Optional.empty();
    }
    ImmutableSet<String> qualifierNames =
        ImmutableSet.copyOf(qualifierMetadata.get().getAsStringList("value"));
    if (qualifierNames.isEmpty()) {
      return Optional.of(ImmutableSet.of());
    }
    ImmutableSet<XAnnotation> qualifierAnnotations =
        element.getAllAnnotations().stream()
            .filter(
                annotation ->
                    qualifierNames.contains(
                        annotation.getType().getTypeElement().getQualifiedName()))
            .collect(toImmutableSet());
    if (qualifierAnnotations.isEmpty()) {
      return Optional.of(ImmutableSet.of());
    }
    // We should be guaranteed that there's exactly one qualifier since the existance of
    // @QualifierMetadata means that this element has already been processed and multiple
    // qualifiers would have been caught already.
    XAnnotation qualifierAnnotation = getOnlyElement(qualifierAnnotations);

    // Ensure the annotation type is superficially valid before we check for @Qualifier, otherwise
    // the @Qualifier marker may appear to be missing from the annotation (b/213880825).
    superficialValidation.validateAnnotationTypeOf(element, qualifierAnnotation);
    if (compilerOptions.strictSuperficialValidation()) {
      return Optional.of(ImmutableSet.of(qualifierAnnotation));
    } else {
      // If strictSuperficialValidation is disabled, then we fall back to the old behavior where
      // we may potentially miss a qualifier rather than report an exception.
      return hasQualifierAnnotation(qualifierAnnotation)
          ? Optional.of(ImmutableSet.of(qualifierAnnotation))
          : Optional.empty();
    }
  }

  /**
   * Returns {@code QualifierMetadata} annotation.
   *
   * <p>Currently, {@code QualifierMetadata} is only associated with inject constructor parameters,
   * inject fields, inject method parameters, provide methods, and provide method parameters.
   */
  private Optional<XAnnotation> getQualifierMetadata(XElement element) {
    return getGeneratedNameForQualifierMetadata(element)
        .flatMap(name -> Optional.ofNullable(processingEnv.findTypeElement(name)))
        .flatMap(type -> Optional.ofNullable(type.getAnnotation(TypeNames.QUALIFIER_METADATA)));
  }

  private Optional<ClassName> getGeneratedNameForQualifierMetadata(XElement element) {
    // Currently we only support @QualifierMetadata for @Inject fields, @Inject method parameters,
    // @Inject constructor parameters, @Provides methods, and @Provides method parameters.
    if (isField(element) && hasInjectAnnotation(element)) {
      return Optional.of(membersInjectorNameForType(closestEnclosingTypeElement(element)));
    } else if (isMethod(element) && element.hasAnnotation(TypeNames.PROVIDES)) {
      return Optional.of(factoryNameForElement(asMethod(element)));
    } else if (isMethodParameter(element)) {
      XExecutableElement executableElement = asMethodParameter(element).getEnclosingElement();
      if (isConstructor(executableElement)
          && hasInjectOrAssistedInjectAnnotation(executableElement)) {
        return Optional.of(factoryNameForElement(executableElement));
      }
      if (isMethod(executableElement) && hasInjectAnnotation(executableElement)) {
        return Optional.of(membersInjectorNameForType(closestEnclosingTypeElement(element)));
      }
      if (isMethod(executableElement) && executableElement.hasAnnotation(TypeNames.PROVIDES)) {
        return Optional.of(factoryNameForElement(executableElement));
      }
    }
    return Optional.empty();
  }

  /** Returns the constructors in {@code type} that are annotated with {@link Inject}. */
  public static ImmutableSet<XConstructorElement> injectedConstructors(XTypeElement type) {
    return type.getConstructors().stream()
        .filter(InjectionAnnotations::hasInjectAnnotation)
        .collect(toImmutableSet());
  }

  private static boolean hasQualifierAnnotation(XAnnotation annotation) {
    return annotation
        .getType()
        .getTypeElement()
        .hasAnyAnnotation(TypeNames.QUALIFIER, TypeNames.QUALIFIER_JAVAX);
  }

  private static boolean hasScopeAnnotation(XAnnotation annotation) {
    return annotation
        .getType()
        .getTypeElement()
        .hasAnyAnnotation(TypeNames.SCOPE, TypeNames.SCOPE_JAVAX);
  }

  /** Returns true if the given element is annotated with {@link Inject}. */
  public static boolean hasInjectAnnotation(XElement element) {
    return element.hasAnyAnnotation(TypeNames.INJECT, TypeNames.INJECT_JAVAX);
  }

  /** Returns true if the given element is annotated with {@link Inject}. */
  public static boolean hasInjectOrAssistedInjectAnnotation(XElement element) {
    return element.hasAnyAnnotation(
        TypeNames.INJECT, TypeNames.INJECT_JAVAX, TypeNames.ASSISTED_INJECT);
  }

  /**
   * Gets the qualifiers annotation of a Kotlin Property. Finding these annotations involve finding
   * the synthetic method for annotations as described by the Kotlin metadata or finding the
   * corresponding MembersInjector method for the field, which also contains the qualifier
   * annotation.
   */
  private ImmutableSet<XAnnotation> getQualifiersForKotlinProperty(XFieldElement field) {
    // TODO(bcorso): Consider moving this to KotlinMetadataUtil
    if (kotlinMetadataUtil.isMissingSyntheticPropertyForAnnotations(field)) {
      // If we detect that the synthetic method for annotations is missing, possibly due to the
      // element being from a compiled class, then find the MembersInjector that was generated
      // for the enclosing class and extract the qualifier information from it.
      XTypeElement membersInjector =
          processingEnv.findTypeElement(
              membersInjectorNameForType(asTypeElement(field.getEnclosingElement())));
      if (membersInjector != null) {
        String memberInjectedFieldSignature = memberInjectedFieldSignatureForVariable(field);
        // TODO(danysantiago): We have to iterate over all the injection methods for every qualifier
        //  look up. Making this N^2 when looking through all the injected fields. :(
        return membersInjector.getDeclaredMethods().stream()
            .filter(
                method ->
                    Optional.ofNullable(method.getAnnotation(TypeNames.INJECTED_FIELD_SIGNATURE))
                        .map(annotation -> annotation.getAsString("value"))
                        .map(memberInjectedFieldSignature::equals)
                        // If a method is not an @InjectedFieldSignature method then filter it out
                        .orElse(false))
            .collect(toOptional())
            .map(this::getQualifiers)
            .orElseThrow(
                () ->
                    new IllegalStateException(
                        String.format(
                            "No matching InjectedFieldSignature for %1$s. This likely means that "
                                + "%1$s was compiled with an older, incompatible version of "
                                + "Dagger. Please update all Dagger dependencies to the same "
                                + "version.",
                            memberInjectedFieldSignature)));
      } else {
        throw new IllegalStateException(
            "No MembersInjector found for " + field.getEnclosingElement());
      }
    } else {
      return Stream.concat(
              kotlinMetadataUtil
                  .getSyntheticPropertyAnnotations(field, TypeNames.QUALIFIER)
                  .stream(),
              kotlinMetadataUtil
                  .getSyntheticPropertyAnnotations(field, TypeNames.QUALIFIER_JAVAX)
                  .stream())
          .collect(toImmutableSet());
    }
  }
}
