/*
 * 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.common.base.CaseFormat;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import dagger.internal.DoubleCheckLazy;
import dagger.internal.codegen.writer.ClassName;
import dagger.internal.codegen.writer.ParameterizedTypeName;
import dagger.internal.codegen.writer.Snippet;
import dagger.internal.codegen.writer.TypeName;
import dagger.internal.codegen.writer.TypeNames;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;

import static com.google.common.base.CaseFormat.UPPER_CAMEL;
import static com.google.common.base.Preconditions.checkArgument;

/**
 * Utilities for generating files.
 *
 * @author Gregory Kick
 * @since 2.0
 */
class SourceFiles {
  /**
   * Sorts {@link DependencyRequest} instances in an order likely to reflect their logical
   * importance.
   */
  static final Ordering<DependencyRequest> DEPENDENCY_ORDERING = new Ordering<DependencyRequest>() {
    @Override
    public int compare(DependencyRequest left, DependencyRequest right) {
      return ComparisonChain.start()
      // put fields before parameters
          .compare(left.requestElement().getKind(), right.requestElement().getKind())
          // order by dependency kind
          .compare(left.kind(), right.kind())
          // then sort by name
          .compare(left.requestElement().getSimpleName().toString(),
              right.requestElement().getSimpleName().toString()).result();
    }
  };

  /**
   * A variant of {@link #indexDependenciesByKey} that maps from unresolved keys
   * to requests.  This is used when generating component's initialize()
   * methods (and in members injectors) in order to instantiate dependent
   * providers.  Consider a generic type of {@code Foo<T>} with a constructor
   * of {@code Foo(T t, T t1, A a, A a1)}.  That will be collapsed to a factory
   * taking a {@code Provider<T> tProvider, Provider<A> aProvider}. However,
   * if it was referenced as {@code Foo<A>}, we need to make sure we still
   * pass two providers.  Naively (if we just referenced by resolved BindingKey),
   * we would have passed a single {@code aProvider}.
   */
  // TODO(user): Refactor these indexing methods so that the binding itself knows what sort of
  // binding keys and framework classes that it needs.
  static ImmutableSetMultimap<BindingKey, DependencyRequest> indexDependenciesByUnresolvedKey(
      Types types, Iterable<? extends DependencyRequest> dependencies) {
    ImmutableSetMultimap.Builder<BindingKey, DependencyRequest> dependenciesByKeyBuilder =
        new ImmutableSetMultimap.Builder<BindingKey, DependencyRequest>()
            .orderValuesBy(DEPENDENCY_ORDERING);
    for (DependencyRequest dependency : dependencies) {
      BindingKey resolved = dependency.bindingKey();
      // To get the proper unresolved type, we have to extract the proper type from the
      // request type again (because we're looking at the actual element's type).
      TypeMirror unresolvedType =
          DependencyRequest.Factory.extractKindAndType(dependency.requestElement().asType()).type();
      BindingKey unresolved =
          BindingKey.create(resolved.kind(), resolved.key().withType(types, unresolvedType));
      dependenciesByKeyBuilder.put(unresolved, dependency);
    }
    return dependenciesByKeyBuilder.build();
  }

  /**
   * Allows dependency requests to be grouped by the key they're requesting.
   * This is used by factory generation in order to minimize the number of parameters
   * required in the case where a given key is requested more than once.  This expects
   * unresolved dependency requests, otherwise we may generate factories based on
   * a particular usage of a class as opposed to the generic types of the class.
   */
  static ImmutableSetMultimap<BindingKey, DependencyRequest> indexDependenciesByKey(
      Iterable<? extends DependencyRequest> dependencies) {
    ImmutableSetMultimap.Builder<BindingKey, DependencyRequest> dependenciesByKeyBuilder =
        new ImmutableSetMultimap.Builder<BindingKey, DependencyRequest>()
            .orderValuesBy(DEPENDENCY_ORDERING);
    for (DependencyRequest dependency : dependencies) {
      dependenciesByKeyBuilder.put(dependency.bindingKey(), dependency);
    }
    return dependenciesByKeyBuilder.build();
  }

  /**
   * This method generates names and keys for the framework classes necessary for all of the
   * bindings. It is responsible for the following:
   * <ul>
   * <li>Choosing a name that associates the binding with all of the dependency requests for this
   * type.
   * <li>Choosing a name that is <i>probably</i> associated with the type being bound.
   * <li>Ensuring that no two bindings end up with the same name.
   * </ul>
   *
   * @return Returns the mapping from {@link BindingKey} to field, sorted by the name of the field.
   */
  static ImmutableMap<BindingKey, FrameworkField> generateBindingFieldsForDependencies(
      DependencyRequestMapper dependencyRequestMapper,
      Iterable<? extends DependencyRequest> dependencies) {
    ImmutableSetMultimap<BindingKey, DependencyRequest> dependenciesByKey =
        indexDependenciesByKey(dependencies);
    Map<BindingKey, Collection<DependencyRequest>> dependenciesByKeyMap =
        dependenciesByKey.asMap();
    ImmutableMap.Builder<BindingKey, FrameworkField> bindingFields = ImmutableMap.builder();
    for (Entry<BindingKey, Collection<DependencyRequest>> entry
        : dependenciesByKeyMap.entrySet()) {
      BindingKey bindingKey = entry.getKey();
      Collection<DependencyRequest> requests = entry.getValue();
      Class<?> frameworkClass =
          dependencyRequestMapper.getFrameworkClass(requests.iterator().next());
      // collect together all of the names that we would want to call the provider
      ImmutableSet<String> dependencyNames =
          FluentIterable.from(requests).transform(new DependencyVariableNamer()).toSet();

      if (dependencyNames.size() == 1) {
        // if there's only one name, great! use it!
        String name = Iterables.getOnlyElement(dependencyNames);
        bindingFields.put(bindingKey,
            FrameworkField.createWithTypeFromKey(frameworkClass, bindingKey, name));
      } else {
        // in the event that a field is being used for a bunch of deps with different names,
        // add all the names together with "And"s in the middle. E.g.: stringAndS
        Iterator<String> namesIterator = dependencyNames.iterator();
        String first = namesIterator.next();
        StringBuilder compositeNameBuilder = new StringBuilder(first);
        while (namesIterator.hasNext()) {
          compositeNameBuilder.append("And").append(
              CaseFormat.LOWER_CAMEL.to(UPPER_CAMEL, namesIterator.next()));
        }
        bindingFields.put(bindingKey, FrameworkField.createWithTypeFromKey(
            frameworkClass, bindingKey, compositeNameBuilder.toString()));
      }
    }
    return bindingFields.build();
  }

  static Snippet frameworkTypeUsageStatement(Snippet frameworkTypeMemberSelect,
      DependencyRequest.Kind dependencyKind) {
    switch (dependencyKind) {
      case LAZY:
        return Snippet.format("%s.create(%s)", ClassName.fromClass(DoubleCheckLazy.class),
            frameworkTypeMemberSelect);
      case INSTANCE:
      case FUTURE:
        return Snippet.format("%s.get()", frameworkTypeMemberSelect);
      case PROVIDER:
      case PRODUCER:
      case MEMBERS_INJECTOR:
        return Snippet.format("%s", frameworkTypeMemberSelect);
      default:
        throw new AssertionError();
    }
  }
  
  /**
   * Returns the generated factory or members injector name for a binding.
   */
  static ClassName generatedClassNameForBinding(Binding binding) {
    switch (binding.bindingType()) {
      case PROVISION:
      case PRODUCTION:
        ContributionBinding contribution = (ContributionBinding) binding;
        checkArgument(!contribution.isSyntheticBinding());
        ClassName enclosingClassName = ClassName.fromTypeElement(contribution.bindingTypeElement());
        switch (contribution.bindingKind()) {
          case INJECTION:
          case PROVISION:
          case IMMEDIATE:
          case FUTURE_PRODUCTION:
            return enclosingClassName
                .topLevelClassName()
                .peerNamed(
                    enclosingClassName.classFileName()
                        + "_"
                        + factoryPrefix(contribution)
                        + "Factory");

          default:
            throw new AssertionError();
        }

      case MEMBERS_INJECTION:
        return membersInjectorNameForType(binding.bindingTypeElement());

      default:
        throw new AssertionError();
    }
  }

  /**
   * Returns the generated factory or members injector name parameterized with the proper type
   * parameters if necessary.
   */
  static TypeName parameterizedGeneratedTypeNameForBinding(Binding binding) {
    return generatedClassNameForBinding(binding).withTypeParameters(bindingTypeParameters(binding));
  }
  
  private static ImmutableList<TypeName> bindingTypeParameters(Binding binding)
      throws AssertionError {
    TypeMirror bindingType;
    switch (binding.bindingType()) {
      case PROVISION:
      case PRODUCTION:
        ContributionBinding contributionBinding = (ContributionBinding) binding;
        if (contributionBinding.contributionType().isMultibinding()) {
          return ImmutableList.of();
        }
        switch (contributionBinding.bindingKind()) {
          case INJECTION:
            bindingType = contributionBinding.key().type();
            break;
            
          case PROVISION:
            // For provision bindings, we parameterize creation on the types of
            // the module, not the types of the binding.
            // Consider: Module<A, B, C> { @Provides List<B> provideB(B b) { .. }}
            // The binding is just parameterized on <B>, but we need all of <A, B, C>.
            bindingType = contributionBinding.bindingTypeElement().asType();
            break;
            
          case IMMEDIATE:
          case FUTURE_PRODUCTION:
            // TODO(beder): Can these be treated just like PROVISION?
            throw new UnsupportedOperationException();
            
          default:
            return ImmutableList.of();
        }
        break;

      case MEMBERS_INJECTION:
        bindingType = binding.key().type();
        break;

      default:
        throw new AssertionError();
    }
    TypeName bindingTypeName = TypeNames.forTypeMirror(bindingType);
    return bindingTypeName instanceof ParameterizedTypeName
        ? ((ParameterizedTypeName) bindingTypeName).parameters()
        : ImmutableList.<TypeName>of();
  }
  
  static ClassName membersInjectorNameForType(TypeElement typeElement) {
    ClassName injectedClassName = ClassName.fromTypeElement(typeElement);
    return injectedClassName
        .topLevelClassName()
        .peerNamed(injectedClassName.classFileName() + "_MembersInjector");
  }

  static ClassName generatedMonitoringModuleName(TypeElement componentElement) {
    ClassName componentName = ClassName.fromTypeElement(componentElement);
    return componentName
        .topLevelClassName()
        .peerNamed(componentName.classFileName() + "_MonitoringModule");
  }

  private static String factoryPrefix(ContributionBinding binding) {
    switch (binding.bindingKind()) {
      case INJECTION:
        return "";

      case PROVISION:
      case IMMEDIATE:
      case FUTURE_PRODUCTION:
        return CaseFormat.LOWER_CAMEL.to(
            UPPER_CAMEL, ((ExecutableElement) binding.bindingElement()).getSimpleName().toString());

      default:
        throw new IllegalArgumentException();
    }
  }

  private SourceFiles() {}
}