/*
 * 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.ComparisonChain;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.SetMultimap;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ElementVisitor;
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.TypeMirror;
import javax.lang.model.util.ElementKindVisitor6;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

import static com.google.auto.common.MoreElements.isAnnotationPresent;
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.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.STATIC;

/**
 * Represents the full members injection of a particular type. This does not pay attention to
 * injected members on supertypes.
 *
 * @author Gregory Kick
 * @since 2.0
 */
@AutoValue
abstract class MembersInjectionBinding extends Binding {
  @Override abstract TypeElement bindingElement();
        
  /** The set of individual sites where {@link Inject} is applied. */
  abstract ImmutableSortedSet<InjectionSite> injectionSites();

  abstract Optional<DependencyRequest> parentInjectorRequest();

  enum Strategy {
    NO_OP,
    INJECT_MEMBERS,
  }

  Strategy injectionStrategy() {
    return injectionSites().isEmpty() ? Strategy.NO_OP : Strategy.INJECT_MEMBERS;
  }

  MembersInjectionBinding withoutParentInjectorRequest() {
    return new AutoValue_MembersInjectionBinding(
          key(),
          dependencies(),
          implicitDependencies(),
          bindingPackage(),
          hasNonDefaultTypeParameters(),
          bindingElement(),
          injectionSites(),
          Optional.<DependencyRequest>absent());
  }

  @Override
  protected Binding.Type bindingType() {
    return Binding.Type.MEMBERS_INJECTION;
  }

  @AutoValue
  abstract static class InjectionSite {
    enum Kind {
      FIELD,
      METHOD,
    }

    abstract Kind kind();

    abstract Element element();

    abstract ImmutableSet<DependencyRequest> dependencies();
    
    protected int indexAmongSiblingMembers(InjectionSite injectionSite) {
      return injectionSite
          .element()
          .getEnclosingElement()
          .getEnclosedElements()
          .indexOf(injectionSite.element());
    }
  }

  static final class Factory {
    private final Elements elements;
    private final Types types;
    private final Key.Factory keyFactory;
    private final DependencyRequest.Factory dependencyRequestFactory;

    Factory(Elements elements, Types types, Key.Factory keyFactory,
        DependencyRequest.Factory dependencyRequestFactory) {
      this.elements = checkNotNull(elements);
      this.types = checkNotNull(types);
      this.keyFactory = checkNotNull(keyFactory);
      this.dependencyRequestFactory = checkNotNull(dependencyRequestFactory);
    }

    private InjectionSite injectionSiteForInjectMethod(
        ExecutableElement methodElement, DeclaredType containingType) {
      checkNotNull(methodElement);
      checkArgument(methodElement.getKind().equals(ElementKind.METHOD));
      ExecutableType resolved =
          MoreTypes.asExecutable(types.asMemberOf(containingType, methodElement));
      return new AutoValue_MembersInjectionBinding_InjectionSite(
          InjectionSite.Kind.METHOD,
          methodElement,
          dependencyRequestFactory.forRequiredResolvedVariables(
              containingType, methodElement.getParameters(), resolved.getParameterTypes()));
    }

    private InjectionSite injectionSiteForInjectField(
        VariableElement fieldElement, DeclaredType containingType) {
      checkNotNull(fieldElement);
      checkArgument(fieldElement.getKind().equals(ElementKind.FIELD));
      checkArgument(isAnnotationPresent(fieldElement, Inject.class));
      TypeMirror resolved = types.asMemberOf(containingType, fieldElement);
      return new AutoValue_MembersInjectionBinding_InjectionSite(
          InjectionSite.Kind.FIELD,
          fieldElement,
          ImmutableSet.of(
              dependencyRequestFactory.forRequiredResolvedVariable(
                  containingType, fieldElement, resolved)));
    }

    /** Returns an unresolved version of this binding. */
    MembersInjectionBinding unresolve(MembersInjectionBinding binding) {
      checkState(binding.hasNonDefaultTypeParameters());
      DeclaredType unresolved = MoreTypes.asDeclared(binding.bindingElement().asType());
      return forInjectedType(unresolved, Optional.<TypeMirror>absent());
    }

    /** Returns true if the type has some injected members in itself or any of its super classes. */
    boolean hasInjectedMembers(DeclaredType declaredType) {
      return !getInjectionSites(declaredType).isEmpty();
    }

    /**
     * Returns a MembersInjectionBinding for the given type. If {@code resolvedType} is present,
     * this will return a resolved binding, with the key & type resolved to the given type (using
     * {@link Types#asMemberOf(DeclaredType, Element)}).
     */
    MembersInjectionBinding forInjectedType(
        DeclaredType declaredType, Optional<TypeMirror> resolvedType) {
      // If the class this is injecting has some type arguments, resolve everything.
      if (!declaredType.getTypeArguments().isEmpty() && resolvedType.isPresent()) {
        DeclaredType resolved = MoreTypes.asDeclared(resolvedType.get());
        // Validate that we're resolving from the correct type.
        checkState(
            types.isSameType(types.erasure(resolved), types.erasure(declaredType)),
            "erased expected type: %s, erased actual type: %s",
            types.erasure(resolved),
            types.erasure(declaredType));
        declaredType = resolved;
      }
      ImmutableSortedSet<InjectionSite> injectionSites = getInjectionSites(declaredType);
      ImmutableSet<DependencyRequest> dependencies =
          FluentIterable.from(injectionSites)
              .transformAndConcat(
                  new Function<InjectionSite, Set<DependencyRequest>>() {
                    @Override
                    public Set<DependencyRequest> apply(InjectionSite input) {
                      return input.dependencies();
                    }
                  })
              .toSet();

      Optional<DependencyRequest> parentInjectorRequest =
          MoreTypes.nonObjectSuperclass(types, elements, declaredType)
              .transform(
                  new Function<DeclaredType, DependencyRequest>() {
                    @Override
                    public DependencyRequest apply(DeclaredType input) {
                      return dependencyRequestFactory.forMembersInjectedType(input);
                    }
                  });

      Key key = keyFactory.forMembersInjectedType(declaredType);
      TypeElement typeElement = MoreElements.asType(declaredType.asElement());
      return new AutoValue_MembersInjectionBinding(
          key,
          dependencies,
          dependencies,
          findBindingPackage(key),
          hasNonDefaultTypeParameters(typeElement, key.type(), types),
          typeElement,
          injectionSites,
          parentInjectorRequest);
    }

    private ImmutableSortedSet<InjectionSite> getInjectionSites(DeclaredType declaredType) {
      Set<InjectionSite> injectionSites = new HashSet<>();
      final List<TypeElement> ancestors = new ArrayList<>();
      SetMultimap<String, ExecutableElement> overriddenMethodMap = LinkedHashMultimap.create();
      for (Optional<DeclaredType> currentType = Optional.of(declaredType);
          currentType.isPresent();
          currentType = MoreTypes.nonObjectSuperclass(types, elements, currentType.get())) {
        final DeclaredType type = currentType.get();
        ancestors.add(MoreElements.asType(type.asElement()));
        for (Element enclosedElement : type.asElement().getEnclosedElements()) {
          Optional<InjectionSite> maybeInjectionSite =
              injectionSiteVisitor.visit(enclosedElement, type);
          if (maybeInjectionSite.isPresent()) {
            InjectionSite injectionSite = maybeInjectionSite.get();
            if (shouldBeInjected(injectionSite.element(), overriddenMethodMap)) {
              injectionSites.add(injectionSite);
            }
            if (injectionSite.kind() == InjectionSite.Kind.METHOD) {
              ExecutableElement injectionSiteMethod =
                  MoreElements.asExecutable(injectionSite.element());
              overriddenMethodMap.put(
                  injectionSiteMethod.getSimpleName().toString(), injectionSiteMethod);
            }
          }
        }
      }
      return ImmutableSortedSet.copyOf(
          new Comparator<InjectionSite>() {
            @Override
            public int compare(InjectionSite left, InjectionSite right) {
              return ComparisonChain.start()
                  // supertypes before subtypes
                  .compare(
                      ancestors.indexOf(right.element().getEnclosingElement()),
                      ancestors.indexOf(left.element().getEnclosingElement()))
                  // fields before methods
                  .compare(left.element().getKind(), right.element().getKind())
                  // then sort by whichever element comes first in the parent
                  // this isn't necessary, but makes the processor nice and predictable
                  .compare(
                      left.indexAmongSiblingMembers(left), right.indexAmongSiblingMembers(right))
                  .result();
            }
          },
          injectionSites);
    }

    private boolean shouldBeInjected(
        Element injectionSite, SetMultimap<String, ExecutableElement> overriddenMethodMap) {
      if (!isAnnotationPresent(injectionSite, Inject.class)
          || injectionSite.getModifiers().contains(PRIVATE)
          || injectionSite.getModifiers().contains(STATIC)) {
        return false;
      }

      if (injectionSite.getKind().isField()) { // Inject all fields (self and ancestors)
        return true;
      }

      // For each method with the same name belonging to any descendant class, return false if any
      // method has already overridden the injectionSite method. To decrease the number of methods
      // that are checked, we store the already injected methods in a SetMultimap and only
      // check the methods with the same name.
      ExecutableElement injectionSiteMethod = MoreElements.asExecutable(injectionSite);
      TypeElement injectionSiteType = MoreElements.asType(injectionSite.getEnclosingElement());
      for (ExecutableElement method :
          overriddenMethodMap.get(injectionSiteMethod.getSimpleName().toString())) {
        if (elements.overrides(method, injectionSiteMethod, injectionSiteType)) {
          return false;
        }
      }
      return true;
    }

    private final ElementVisitor<Optional<InjectionSite>, DeclaredType> injectionSiteVisitor =
        new ElementKindVisitor6<Optional<InjectionSite>, DeclaredType>(
            Optional.<InjectionSite>absent()) {
          @Override
          public Optional<InjectionSite> visitExecutableAsMethod(
              ExecutableElement e, DeclaredType type) {
            return Optional.of(injectionSiteForInjectMethod(e, type));
          }

          @Override
          public Optional<InjectionSite> visitVariableAsField(
              VariableElement e, DeclaredType type) {
            return (isAnnotationPresent(e, Inject.class)
                    && !e.getModifiers().contains(PRIVATE)
                    && !e.getModifiers().contains(STATIC))
                ? Optional.of(injectionSiteForInjectField(e, type))
                : Optional.<InjectionSite>absent();
          }
        };
  }
}