/*
 * Copyright (C) 2014 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.MoreElements;
import com.google.auto.common.MoreTypes;
import com.google.auto.value.AutoValue;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.ListenableFuture;
import dagger.Lazy;
import dagger.MembersInjector;
import dagger.Provides;
import dagger.producers.Produced;
import dagger.producers.Producer;
import dagger.producers.internal.AbstractProducer;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;

import static com.google.auto.common.MoreTypes.isTypeOf;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static javax.lang.model.type.TypeKind.DECLARED;
import static javax.lang.model.util.ElementFilter.constructorsIn;

/**
 * Represents a request for a key at an injection point. Parameters to {@link Inject} constructors
 * or {@link Provides} methods are examples of key requests.
 *
 * @author Gregory Kick
 * @since 2.0
 */
// TODO(gak): Set bindings and the permutations thereof need to be addressed
@AutoValue
abstract class DependencyRequest {
  static final Function<DependencyRequest, BindingKey> BINDING_KEY_FUNCTION =
      new Function<DependencyRequest, BindingKey>() {
        @Override public BindingKey apply(DependencyRequest request) {
          return request.bindingKey();
        }
      };

  enum Kind {
    /** A default request for an instance.  E.g.: {@code Blah} */
    INSTANCE,
    /** A request for a {@link Provider}.  E.g.: {@code Provider<Blah>} */
    PROVIDER,
    /** A request for a {@link Lazy}.  E.g.: {@code Lazy<Blah>} */
    LAZY,
    /** A request for a {@link MembersInjector}.  E.g.: {@code MembersInjector<Blah>} */
    MEMBERS_INJECTOR,
    /** A request for a {@link Producer}.  E.g.: {@code Producer<Blah>} */
    PRODUCER,
    /** A request for a {@link Produced}.  E.g.: {@code Produced<Blah>} */
    PRODUCED,
    /**
     * A request for a {@link ListenableFuture}.  E.g.: {@code ListenableFuture<Blah>}.
     * These can only be requested by component interfaces.
     */
    FUTURE,
  }

  abstract Kind kind();
  abstract Key key();

  BindingKey bindingKey() {
    switch (kind()) {
      case INSTANCE:
      case LAZY:
      case PROVIDER:
      case PRODUCER:
      case PRODUCED:
      case FUTURE:
        return BindingKey.create(BindingKey.Kind.CONTRIBUTION, key());
      case MEMBERS_INJECTOR:
        return BindingKey.create(BindingKey.Kind.MEMBERS_INJECTION, key());
      default:
        throw new AssertionError();
    }
  }

  abstract Element requestElement();

  /**
   * Returns the possibly resolved type that contained the requesting element. For members injection
   * requests, this is the type itself.
   */
  abstract DeclaredType enclosingType();

  /** Returns true if this request allows null objects. */
  abstract boolean isNullable();

  /**
   * An optional name for this request when it's referred to in generated code. If absent, it will
   * use a name derived from {@link #requestElement}.
   */
  abstract Optional<String> overriddenVariableName();

  /**
   * Factory for {@link DependencyRequest}s.
   *
   * <p>Any factory method may throw {@link TypeNotPresentException} if a type is not available,
   * which may mean that the type will be generated in a later round of processing.
   */
  static final class Factory {
    private final Elements elements;
    private final Key.Factory keyFactory;

    Factory(Elements elements, Key.Factory keyFactory) {
      this.elements = elements;
      this.keyFactory = keyFactory;
    }

    ImmutableSet<DependencyRequest> forRequiredResolvedVariables(DeclaredType container,
        List<? extends VariableElement> variables, List<? extends TypeMirror> resolvedTypes) {
      checkState(resolvedTypes.size() == variables.size());
      ImmutableSet.Builder<DependencyRequest> builder = ImmutableSet.builder();
      for (int i = 0; i < variables.size(); i++) {
        builder.add(forRequiredResolvedVariable(container, variables.get(i), resolvedTypes.get(i)));
      }
      return builder.build();
    }

    ImmutableSet<DependencyRequest> forRequiredVariables(
        List<? extends VariableElement> variables) {
      return FluentIterable.from(variables)
          .transform(
              new Function<VariableElement, DependencyRequest>() {
                @Override
                public DependencyRequest apply(VariableElement input) {
                  return forRequiredVariable(input);
                }
              })
          .toSet();
    }

    /**
     * Creates a implicit {@link DependencyRequest} for {@code mapOfFactoryKey}, which will be used
     * to satisfy the {@code mapOfValueRequest}.
     * 
     * @param mapOfValueRequest a request for {@code Map<K, V>}
     * @param mapOfFactoryKey a key equivalent to {@code mapOfValueRequest}'s key, whose type is
     *     {@code Map<K, Provider<V>>} or {@code Map<K, Producer<V>>}
     */
    DependencyRequest forImplicitMapBinding(
        DependencyRequest mapOfValueRequest, Key mapOfFactoryKey) {
      checkNotNull(mapOfValueRequest);
      return new AutoValue_DependencyRequest(
          Kind.PROVIDER,
          mapOfFactoryKey,
          mapOfValueRequest.requestElement(),
          mapOfValueRequest.enclosingType(),
          false /* doesn't allow null */,
          Optional.<String>absent());
    }

    DependencyRequest forRequiredVariable(VariableElement variableElement) {
      return forRequiredVariable(variableElement, Optional.<String>absent());
    }

    DependencyRequest forRequiredVariable(VariableElement variableElement, Optional<String> name) {
      checkNotNull(variableElement);
      TypeMirror type = variableElement.asType();
      Optional<AnnotationMirror> qualifier = InjectionAnnotations.getQualifier(variableElement);
      return newDependencyRequest(
          variableElement, type, qualifier, getEnclosingType(variableElement), name);
    }

    DependencyRequest forRequiredResolvedVariable(DeclaredType container,
        VariableElement variableElement,
        TypeMirror resolvedType) {
      checkNotNull(variableElement);
      checkNotNull(resolvedType);
      Optional<AnnotationMirror> qualifier = InjectionAnnotations.getQualifier(variableElement);
      return newDependencyRequest(
          variableElement, resolvedType, qualifier, container, Optional.<String>absent());
    }

    DependencyRequest forComponentProvisionMethod(ExecutableElement provisionMethod,
        ExecutableType provisionMethodType) {
      checkNotNull(provisionMethod);
      checkNotNull(provisionMethodType);
      checkArgument(
          provisionMethod.getParameters().isEmpty(),
          "Component provision methods must be empty: %s",
          provisionMethod);
      Optional<AnnotationMirror> qualifier = InjectionAnnotations.getQualifier(provisionMethod);
      return newDependencyRequest(
          provisionMethod,
          provisionMethodType.getReturnType(),
          qualifier,
          getEnclosingType(provisionMethod),
          Optional.<String>absent());
    }

    DependencyRequest forComponentProductionMethod(ExecutableElement productionMethod,
        ExecutableType productionMethodType) {
      checkNotNull(productionMethod);
      checkNotNull(productionMethodType);
      checkArgument(productionMethod.getParameters().isEmpty(),
          "Component production methods must be empty: %s", productionMethod);
      TypeMirror type = productionMethodType.getReturnType();
      Optional<AnnotationMirror> qualifier = InjectionAnnotations.getQualifier(productionMethod);
      DeclaredType container = getEnclosingType(productionMethod);
      // Only a component production method can be a request for a ListenableFuture, so we
      // special-case it here.
      if (isTypeOf(ListenableFuture.class, type)) {
        return new AutoValue_DependencyRequest(
            Kind.FUTURE,
            keyFactory.forQualifiedType(
                qualifier, Iterables.getOnlyElement(((DeclaredType) type).getTypeArguments())),
            productionMethod,
            container,
            false /* doesn't allow null */,
            Optional.<String>absent());
      } else {
        return newDependencyRequest(
            productionMethod, type, qualifier, container, Optional.<String>absent());
      }
    }

    DependencyRequest forComponentMembersInjectionMethod(ExecutableElement membersInjectionMethod,
        ExecutableType membersInjectionMethodType) {
      checkNotNull(membersInjectionMethod);
      checkNotNull(membersInjectionMethodType);
      Optional<AnnotationMirror> qualifier =
          InjectionAnnotations.getQualifier(membersInjectionMethod);
      checkArgument(!qualifier.isPresent());
      TypeMirror returnType = membersInjectionMethodType.getReturnType();
      if (returnType.getKind().equals(DECLARED)
          && MoreTypes.isTypeOf(MembersInjector.class, returnType)) {
        return new AutoValue_DependencyRequest(
            Kind.MEMBERS_INJECTOR,
            keyFactory.forMembersInjectedType(
                Iterables.getOnlyElement(((DeclaredType) returnType).getTypeArguments())),
            membersInjectionMethod,
            getEnclosingType(membersInjectionMethod),
            false /* doesn't allow null */,
            Optional.<String>absent());
      } else {
        return new AutoValue_DependencyRequest(
            Kind.MEMBERS_INJECTOR,
            keyFactory.forMembersInjectedType(
                Iterables.getOnlyElement(membersInjectionMethodType.getParameterTypes())),
            membersInjectionMethod,
            getEnclosingType(membersInjectionMethod),
            false /* doesn't allow null */,
            Optional.<String>absent());
      }
    }

    DependencyRequest forMembersInjectedType(DeclaredType type) {
      return new AutoValue_DependencyRequest(
          Kind.MEMBERS_INJECTOR,
          keyFactory.forMembersInjectedType(type),
          type.asElement(),
          type,
          false /* doesn't allow null */,
          Optional.<String>absent());
    }

    DependencyRequest forProductionComponentMonitorProvider() {
      TypeElement element = elements.getTypeElement(AbstractProducer.class.getCanonicalName());
      for (ExecutableElement constructor : constructorsIn(element.getEnclosedElements())) {
        if (constructor.getParameters().size() == 2) {
          // the 2-arg constructor has the appropriate dependency as its first arg
          return forRequiredVariable(constructor.getParameters().get(0), Optional.of("monitor"));
        }
      }
      throw new AssertionError("expected 2-arg constructor in AbstractProducer");
    }

    private DependencyRequest newDependencyRequest(
        Element requestElement,
        TypeMirror type,
        Optional<AnnotationMirror> qualifier,
        DeclaredType container,
        Optional<String> name) {
      KindAndType kindAndType = extractKindAndType(type);
      if (kindAndType.kind().equals(Kind.MEMBERS_INJECTOR)) {
        checkArgument(!qualifier.isPresent());
      }
      // Only instance types can be non-null -- all other requests are wrapped
      // inside something (e.g, Provider, Lazy, etc..).
      // TODO(sameb): should Produced/Producer always require non-nullable?
      boolean allowsNull = !kindAndType.kind().equals(Kind.INSTANCE)
          || ConfigurationAnnotations.getNullableType(requestElement).isPresent();
      return new AutoValue_DependencyRequest(
          kindAndType.kind(),
          keyFactory.forQualifiedType(qualifier, kindAndType.type()),
          requestElement,
          container,
          allowsNull,
          name);
    }

    @AutoValue
    static abstract class KindAndType {
      abstract Kind kind();
      abstract TypeMirror type();
    }

    /**
     * Extracts the correct requesting type & kind out a request type. For example, if a user
     * requests {@code Provider<Foo>}, this will return ({@link Kind#PROVIDER}, {@code Foo}).
     *
     * @throws TypeNotPresentException if {@code type}'s kind is {@link TypeKind#ERROR}, which may
     *     mean that the type will be generated in a later round of processing
     */
    static KindAndType extractKindAndType(TypeMirror type) {
      if (type.getKind().equals(TypeKind.ERROR)) {
        throw new TypeNotPresentException(type.toString(), null);
      }

      // We must check TYPEVAR explicitly before the below checks because calling
      // isTypeOf(..) on a TYPEVAR throws an exception (because it can't be
      // represented as a Class).
      if (type.getKind().equals(TypeKind.TYPEVAR)) {
        return new AutoValue_DependencyRequest_Factory_KindAndType(Kind.INSTANCE, type);
      } else if (isTypeOf(Provider.class, type)) {
        return new AutoValue_DependencyRequest_Factory_KindAndType(Kind.PROVIDER,
            Iterables.getOnlyElement(((DeclaredType) type).getTypeArguments()));
      } else if (isTypeOf(Lazy.class, type)) {
        return new AutoValue_DependencyRequest_Factory_KindAndType(Kind.LAZY,
            Iterables.getOnlyElement(((DeclaredType) type).getTypeArguments()));
      } else if (isTypeOf(MembersInjector.class, type)) {
        return new AutoValue_DependencyRequest_Factory_KindAndType(Kind.MEMBERS_INJECTOR,
            Iterables.getOnlyElement(((DeclaredType) type).getTypeArguments()));
      } else if (isTypeOf(Producer.class, type)) {
        return new AutoValue_DependencyRequest_Factory_KindAndType(Kind.PRODUCER,
            Iterables.getOnlyElement(((DeclaredType) type).getTypeArguments()));
      } else if (isTypeOf(Produced.class, type)) {
        return new AutoValue_DependencyRequest_Factory_KindAndType(Kind.PRODUCED,
            Iterables.getOnlyElement(((DeclaredType) type).getTypeArguments()));
      } else {
        return new AutoValue_DependencyRequest_Factory_KindAndType(Kind.INSTANCE, type);
      }
    }

    static DeclaredType getEnclosingType(Element element) {
      while (!MoreElements.isType(element)) {
        element = element.getEnclosingElement();
      }
      return MoreTypes.asDeclared(element.asType());
    }
  }
}