package org.checkerframework.framework.type.poly;

import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewClassTree;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedIntersectionType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedNullType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedPrimitiveType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedUnionType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedWildcardType;
import org.checkerframework.framework.type.QualifierHierarchy;
import org.checkerframework.framework.type.visitor.EquivalentAtmComboScanner;
import org.checkerframework.framework.type.visitor.SimpleAnnotatedTypeScanner;
import org.checkerframework.framework.util.AnnotatedTypes;
import org.checkerframework.javacutil.AnnotationMirrorMap;
import org.checkerframework.javacutil.AnnotationMirrorSet;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.TreeUtils;
import org.checkerframework.javacutil.TypesUtils;
import org.plumelib.util.CollectionsPlume;

/**
 * Implements framework support for qualifier polymorphism.
 *
 * <p>{@link DefaultQualifierPolymorphism} implements the abstract methods in this class. Subclasses
 * can alter the way instantiations of polymorphic qualifiers are {@link #combine combined}.
 *
 * <p>An "instantiation" is a mapping from declaration type to use-site type &mdash; that is, a
 * mapping from {@code @Poly*} to concrete qualifiers.
 *
 * <p>The implementation performs these steps:
 *
 * <ul>
 *   <li>the PolyCollector creates an instantiation
 *   <li>if the instantiation is non-empty: the Replacer does resolution -- that is, it replaces
 *       each occurrence of {@code @Poly*} by the concrete qualifier it maps to in the instantiation
 *   <li>if the instantiation is empty, the Completer replaces each {@code @Poly*} by the bottom
 *       qualifier
 * </ul>
 */
public abstract class AbstractQualifierPolymorphism implements QualifierPolymorphism {

  /** Annotated type factory. */
  protected final AnnotatedTypeFactory atypeFactory;

  /** The qualifier hierarchy to use. */
  protected final QualifierHierarchy qualHierarchy;

  /**
   * The polymorphic qualifiers: mapping from a polymorphic qualifier of {@code qualHierarchy} to
   * the top qualifier of that hierarchy.
   */
  protected final AnnotationMirrorMap<AnnotationMirror> polyQuals = new AnnotationMirrorMap<>();

  /**
   * The qualifiers at the top of {@code qualHierarchy}. These are the values in {@code polyQuals}.
   */
  protected final AnnotationMirrorSet topQuals;

  /** Determines the instantiations for each polymorphic qualifier. */
  private final PolyCollector collector = new PolyCollector();

  /** Resolves each polymorphic qualifier by replacing it with its instantiation. */
  private final SimpleAnnotatedTypeScanner<Void, AnnotationMirrorMap<AnnotationMirror>> replacer;

  /**
   * Completes a type by removing any unresolved polymorphic qualifiers, replacing them with the
   * bottom qualifiers.
   *
   * <p>This is only called when {@code instantiationMapping} is empty. (And that implies that there
   * are no polymorphic qualifiers on formal parameters??)
   */
  private final SimpleAnnotatedTypeScanner<Void, Void> completer;

  /** Mapping from poly qualifier to its instantiation for types with a qualifier parameter. */
  protected final AnnotationMirrorMap<AnnotationMirror> polyInstantiationForQualifierParameter =
      new AnnotationMirrorMap<>();

  /** The visit method returns true if the passed type has any polymorphic qualifiers. */
  protected final SimpleAnnotatedTypeScanner<Boolean, Void> hasPolyScanner;

  /**
   * Creates an {@link AbstractQualifierPolymorphism} instance that uses the given checker for
   * querying type qualifiers and the given factory for getting annotated types. Subclasses need to
   * add polymorphic qualifiers to {@code this.polyQuals}.
   *
   * @param env the processing environment
   * @param factory the factory for the current checker
   */
  protected AbstractQualifierPolymorphism(ProcessingEnvironment env, AnnotatedTypeFactory factory) {
    this.atypeFactory = factory;
    this.qualHierarchy = factory.getQualifierHierarchy();
    this.topQuals = new AnnotationMirrorSet(qualHierarchy.getTopAnnotations());

    this.completer =
        new SimpleAnnotatedTypeScanner<>(
            (type, p) -> {
              for (Map.Entry<AnnotationMirror, AnnotationMirror> entry : polyQuals.entrySet()) {
                AnnotationMirror poly = entry.getKey();
                AnnotationMirror top = entry.getValue();
                if (type.hasPrimaryAnnotation(poly)) {
                  type.removePrimaryAnnotation(poly);
                  if (type.getKind() != TypeKind.TYPEVAR && type.getKind() != TypeKind.WILDCARD) {
                    // Do not add qualifiers to type variables and
                    // wildcards
                    type.addAnnotation(this.qualHierarchy.getBottomAnnotation(top));
                  }
                }
              }
              return null;
            });

    this.replacer =
        new SimpleAnnotatedTypeScanner<>(
            (type, map) -> {
              replace(type, map);
              return null;
            });

    this.hasPolyScanner =
        new SimpleAnnotatedTypeScanner<>(
            (type, notused) -> {
              for (AnnotationMirror a : type.getPrimaryAnnotations()) {
                if (qualHierarchy.isPolymorphicQualifier(a)) {
                  return true;
                }
              }
              return false;
            },
            Boolean::logicalOr,
            false);
  }

  /**
   * Reset to allow reuse of the same instance. Subclasses should override this method. The
   * overriding implementation should clear its additional state and then call the super
   * implementation.
   */
  protected void reset() {
    collector.reset();
    replacer.reset();
    completer.reset();
    polyInstantiationForQualifierParameter.clear();
  }

  @Override
  public boolean hasPolymorphicQualifiers(AnnotatedTypeMirror type) {
    return hasPolyScanner.visit(type);
  }

  /**
   * Resolves polymorphism annotations for the given type.
   *
   * @param tree the tree associated with the type
   * @param type the type to annotate
   */
  @Override
  public void resolve(MethodInvocationTree tree, AnnotatedExecutableType type) {
    if (polyQuals.isEmpty() || !hasPolymorphicQualifiers(type)) {
      return;
    }

    // javac produces enum super calls with zero arguments even though the
    // method element requires two.
    // See also BaseTypeVisitor.visitMethodInvocation and
    // CFGBuilder.CFGTranslationPhaseOne.visitMethodInvocation.
    if (TreeUtils.isEnumSuperCall(tree)) {
      return;
    }
    List<AnnotatedTypeMirror> parameters =
        AnnotatedTypes.adaptParameters(atypeFactory, type, tree.getArguments(), tree);
    List<AnnotatedTypeMirror> arguments =
        CollectionsPlume.mapList(atypeFactory::getAnnotatedType, tree.getArguments());

    AnnotationMirrorMap<AnnotationMirror> instantiationMapping =
        collector.visit(arguments, parameters);

    // For super() and this() method calls, getReceiverType(tree) does not return the correct
    // type. So, just skip those.  This is consistent with skipping receivers of constructors
    // below.
    if (type.getReceiverType() != null
        && !TreeUtils.isSuperConstructorCall(tree)
        && !TreeUtils.isThisConstructorCall(tree)) {
      instantiationMapping =
          collector.reduce(
              instantiationMapping,
              collector.visit(atypeFactory.getReceiverType(tree), type.getReceiverType()));
    }

    if ((instantiationMapping != null && !instantiationMapping.isEmpty())
        || TreeUtils.isCallToVarargsMethodWithZeroVarargsActuals(tree)) {
      replacer.visit(type, instantiationMapping);
    } else {
      completer.visit(type);
    }
    reset();
  }

  @Override
  public void resolve(NewClassTree tree, AnnotatedExecutableType type) {
    if (polyQuals.isEmpty() || !hasPolymorphicQualifiers(type)) {
      return;
    }

    List<AnnotatedTypeMirror> parameters =
        AnnotatedTypes.adaptParameters(atypeFactory, type, tree.getArguments(), tree);
    List<AnnotatedTypeMirror> arguments =
        CollectionsPlume.mapList(atypeFactory::getAnnotatedType, tree.getArguments());

    AnnotationMirrorMap<AnnotationMirror> instantiationMapping =
        collector.visit(arguments, parameters);
    // TODO: poly on receiver for constructors?
    // instantiationMapping = collector.reduce(instantiationMapping,
    //        collector.visit(factory.getReceiverType(tree), type.getReceiverType()));

    AnnotatedTypeMirror newClassType = type.getReturnType().deepCopy();
    newClassType.clearPrimaryAnnotations();
    newClassType.replaceAnnotations(atypeFactory.getExplicitNewClassAnnos(tree));

    instantiationMapping =
        collector.reduce(
            instantiationMapping, mapQualifierToPoly(newClassType, type.getReturnType()));

    if (instantiationMapping != null && !instantiationMapping.isEmpty()) {
      replacer.visit(type, instantiationMapping);
    } else {
      completer.visit(type);
    }
    reset();
  }

  @Override
  public void resolve(VariableElement field, AnnotatedTypeMirror owner, AnnotatedTypeMirror type) {
    if (polyQuals.isEmpty() || !hasPolymorphicQualifiers(type)) {
      return;
    }
    AnnotationMirrorMap<AnnotationMirror> matchingMapping = new AnnotationMirrorMap<>();
    polyQuals.forEach(
        (polyAnnotation, topAnno) -> {
          AnnotationMirror annoOnOwner = owner.getPrimaryAnnotationInHierarchy(topAnno);
          if (annoOnOwner != null) {
            matchingMapping.put(polyAnnotation, annoOnOwner);
          }
        });
    if (!matchingMapping.isEmpty()) {
      replacer.visit(type, matchingMapping);
    } else {
      completer.visit(type);
    }
    reset();
  }

  @Override
  public void resolve(
      AnnotatedExecutableType functionalInterface, AnnotatedExecutableType memberReference) {
    if (hasPolymorphicQualifiers(functionalInterface.getReturnType())) {
      // functional interface has a polymorphic qualifier, so they should not be resolved
      // on memberReference.
      return;
    }
    if (polyQuals.isEmpty() || !hasPolymorphicQualifiers(memberReference)) {
      return;
    }
    AnnotationMirrorMap<AnnotationMirror> instantiationMapping;

    List<AnnotatedTypeMirror> parameters = memberReference.getParameterTypes();
    List<AnnotatedTypeMirror> args = functionalInterface.getParameterTypes();
    if (args.size() == parameters.size() + 1) {
      // If the member reference is a reference to an instance method of an arbitrary
      // object, then first parameter of the functional interface corresponds to the
      // receiver of the member reference.
      List<AnnotatedTypeMirror> newParameters = new ArrayList<>(parameters.size() + 1);
      newParameters.add(memberReference.getReceiverType());
      newParameters.addAll(parameters);
      parameters = newParameters;
      instantiationMapping = new AnnotationMirrorMap<>();
    } else {
      if (memberReference.getReceiverType() != null
          && functionalInterface.getReceiverType() != null) {
        instantiationMapping =
            mapQualifierToPoly(
                functionalInterface.getReceiverType(), memberReference.getReceiverType());
      } else {
        instantiationMapping = new AnnotationMirrorMap<>();
      }
    }
    // Deal with varargs
    if (memberReference.isVarargs() && !functionalInterface.isVarargs()) {
      parameters = AnnotatedTypes.expandVarargsParametersFromTypes(memberReference, args);
    }

    instantiationMapping =
        collector.reduce(instantiationMapping, collector.visit(args, parameters));

    if (instantiationMapping != null && !instantiationMapping.isEmpty()) {
      replacer.visit(memberReference, instantiationMapping);
    } else {
      // TODO: Do we need this (return type?)
      completer.visit(memberReference);
    }
    reset();
  }

  /**
   * If the primary annotation of {@code polyType} is a polymorphic qualifier, then it is mapped to
   * the primary annotation of {@code type} and the map is returned. Otherwise, an empty map is
   * returned.
   *
   * @param type type with qualifier to us in the map
   * @param polyType type that may have polymorphic qualifiers
   * @return a mapping from the polymorphic qualifiers in {@code polyType} to the qualifiers in
   *     {@code type}
   */
  private AnnotationMirrorMap<AnnotationMirror> mapQualifierToPoly(
      AnnotatedTypeMirror type, AnnotatedTypeMirror polyType) {
    AnnotationMirrorMap<AnnotationMirror> result = new AnnotationMirrorMap<>();

    for (Map.Entry<AnnotationMirror, AnnotationMirror> kv : polyQuals.entrySet()) {
      AnnotationMirror top = kv.getValue();
      AnnotationMirror poly = kv.getKey();
      if (polyType.hasPrimaryAnnotation(poly)) {
        AnnotationMirror typeQual = type.getPrimaryAnnotationInHierarchy(top);
        if (typeQual != null) {
          if (atypeFactory.hasQualifierParameterInHierarchy(type, top)) {
            polyInstantiationForQualifierParameter.put(poly, typeQual);
          }
          result.put(poly, typeQual);
        }
      }
    }
    return result;
  }

  /**
   * Returns annotation that is the combination of the two annotations. The annotations are
   * instantiations for {@code polyQual}.
   *
   * <p>The combination is typically their least upper bound. (It could be the GLB in the case that
   * all arguments to a polymorphic method must have the same annotation.)
   *
   * @param polyQual polymorphic qualifier for which {@code a1} and {@code a2} are instantiations
   * @param a1 an annotation that is an instantiation of {@code polyQual}
   * @param a2 an annotation that is an instantiation of {@code polyQual}
   * @return an annotation that is the combination of the two annotations
   */
  protected abstract AnnotationMirror combine(
      AnnotationMirror polyQual, AnnotationMirror a1, AnnotationMirror a2);

  /**
   * Replaces the top-level polymorphic annotations in {@code type} with the instantiations in
   * {@code replacements}.
   *
   * <p>This method is called on all parts of a type.
   *
   * @param type the AnnotatedTypeMirror whose poly annotations are replaced; it is side-effected by
   *     this method
   * @param replacements a mapping from polymorphic annotation to instantiation
   */
  protected abstract void replace(
      AnnotatedTypeMirror type, AnnotationMirrorMap<AnnotationMirror> replacements);

  /**
   * A helper class that resolves the polymorphic qualifiers with the most restrictive qualifier. It
   * returns a mapping from the polymorphic qualifier to the substitution for that qualifier.
   */
  private class PolyCollector
      extends EquivalentAtmComboScanner<AnnotationMirrorMap<AnnotationMirror>, Void> {

    /**
     * Set of {@link AnnotatedTypeVariable} or {@link AnnotatedWildcardType} that have been visited.
     * Used to prevent infinite recursion on recursive types.
     *
     * <p>Uses reference equality rather than equals because the visitor may visit two types that
     * are structurally equal, but not actually the same. For example, the wildcards in {@code
     * IPair<?,?>} may be equal, but they both should be visited.
     */
    private final Set<AnnotatedTypeMirror> visitedTypes =
        Collections.newSetFromMap(new IdentityHashMap<AnnotatedTypeMirror, Boolean>());

    /**
     * Returns true if the {@link AnnotatedTypeMirror} has been visited. If it has not, then it is
     * added to the list of visited AnnotatedTypeMirrors.
     */
    private boolean visited(AnnotatedTypeMirror atm) {
      return !visitedTypes.add(atm);
    }

    @Override
    protected AnnotationMirrorMap<AnnotationMirror> scanWithNull(
        AnnotatedTypeMirror type1, AnnotatedTypeMirror type2, Void aVoid) {
      return new AnnotationMirrorMap<>();
    }

    @Override
    public AnnotationMirrorMap<AnnotationMirror> reduce(
        AnnotationMirrorMap<AnnotationMirror> r1, AnnotationMirrorMap<AnnotationMirror> r2) {

      if (r1 == null || r1.isEmpty()) {
        return r2;
      }
      if (r2 == null || r2.isEmpty()) {
        return r1;
      }

      AnnotationMirrorMap<AnnotationMirror> res = new AnnotationMirrorMap<>();
      // Ensure that all qualifiers from r1 and r2 are visited.
      AnnotationMirrorSet r2remain = new AnnotationMirrorSet();
      r2remain.addAll(r2.keySet());
      for (Map.Entry<AnnotationMirror, AnnotationMirror> entry : r1.entrySet()) {
        AnnotationMirror polyQual = entry.getKey();
        AnnotationMirror a1Annos = entry.getValue();
        AnnotationMirror a2Annos = r2.get(polyQual);
        if (a2Annos == null) {
          res.put(polyQual, a1Annos);
        } else {
          res.put(polyQual, combine(polyQual, a1Annos, a2Annos));
        }
        r2remain.remove(polyQual);
      }
      for (AnnotationMirror key2 : r2remain) {
        res.put(key2, r2.get(key2));
      }
      return res;
    }

    /**
     * Calls {@link #visit(AnnotatedTypeMirror, AnnotatedTypeMirror)} for each type in {@code
     * types}.
     *
     * @param types the AnnotateTypeMirrors used to find instantiations
     * @param polyTypes the AnnotatedTypeMirrors that may have polymorphic qualifiers
     * @return a mapping of polymorphic qualifiers to their instantiations
     */
    private AnnotationMirrorMap<AnnotationMirror> visit(
        Iterable<? extends AnnotatedTypeMirror> types,
        Iterable<? extends AnnotatedTypeMirror> polyTypes) {
      AnnotationMirrorMap<AnnotationMirror> result = new AnnotationMirrorMap<>();

      Iterator<? extends AnnotatedTypeMirror> itert = types.iterator();
      Iterator<? extends AnnotatedTypeMirror> itera = polyTypes.iterator();

      while (itert.hasNext() && itera.hasNext()) {
        AnnotatedTypeMirror type = itert.next();
        AnnotatedTypeMirror actualType = itera.next();
        result = reduce(result, visit(type, actualType));
      }
      if (itert.hasNext()) {
        throw new BugInCF(
            "PolyCollector.visit: types is longer than polyTypes:%n"
                + "  types = %s%n  polyTypes = %s%n",
            types, polyTypes);
      }
      if (itera.hasNext()) {
        throw new BugInCF(
            "PolyCollector.visit: types is shorter than polyTypes:%n"
                + "  types = %s%n  polyTypes = %s%n",
            types, polyTypes);
      }
      return result;
    }

    /**
     * Creates a mapping of polymorphic qualifiers to their instantiations by visiting each
     * composite type in {@code type}.
     *
     * @param type the AnnotatedTypeMirror used to find instantiations
     * @param polyType the AnnotatedTypeMirror that may have polymorphic qualifiers
     * @return a mapping of polymorphic qualifiers to their instantiations
     */
    private AnnotationMirrorMap<AnnotationMirror> visit(
        AnnotatedTypeMirror type, AnnotatedTypeMirror polyType) {
      if (type.getKind() == TypeKind.NULL) {
        return mapQualifierToPoly(type, polyType);
      }

      if (type.getKind() == TypeKind.WILDCARD) {
        AnnotatedWildcardType wildcardType = (AnnotatedWildcardType) type;
        if (wildcardType.getExtendsBound().getKind() == TypeKind.WILDCARD) {
          wildcardType = (AnnotatedWildcardType) wildcardType.getExtendsBound();
        }
        if (wildcardType.isTypeArgOfRawType()) {
          return mapQualifierToPoly(wildcardType.getExtendsBound(), polyType);
        }

        switch (polyType.getKind()) {
          case WILDCARD:
            AnnotatedTypeMirror asSuper =
                AnnotatedTypes.asSuper(atypeFactory, wildcardType, polyType);
            return visit(asSuper, polyType, null);
          case TYPEVAR:
            return mapQualifierToPoly(wildcardType.getExtendsBound(), polyType);
          default:
            return mapQualifierToPoly(wildcardType.getExtendsBound(), polyType);
        }
      }

      AnnotatedTypeMirror asSuper = AnnotatedTypes.asSuper(atypeFactory, type, polyType);

      return visit(asSuper, polyType, null);
    }

    @Override
    public AnnotationMirrorMap<AnnotationMirror> visitArray_Array(
        AnnotatedArrayType type1, AnnotatedArrayType type2, Void aVoid) {
      AnnotationMirrorMap<AnnotationMirror> result = mapQualifierToPoly(type1, type2);
      return reduce(result, super.visitArray_Array(type1, type2, aVoid));
    }

    @Override
    public AnnotationMirrorMap<AnnotationMirror> visitDeclared_Declared(
        AnnotatedDeclaredType type1, AnnotatedDeclaredType type2, Void aVoid) {
      // Don't call super because asSuper has to be called on each type argument.
      if (visited(type2)) {
        return new AnnotationMirrorMap<>();
      }

      AnnotationMirrorMap<AnnotationMirror> result = mapQualifierToPoly(type1, type2);

      Iterator<AnnotatedTypeMirror> type2Args = type2.getTypeArguments().iterator();
      for (AnnotatedTypeMirror type1Arg : type1.getTypeArguments()) {
        AnnotatedTypeMirror type2Arg = type2Args.next();
        if (TypesUtils.isErasedSubtype(
            type1Arg.getUnderlyingType(),
            type2Arg.getUnderlyingType(),
            atypeFactory.getChecker().getTypeUtils())) {
          result = reduce(result, visit(type1Arg, type2Arg));
        } // else an unchecked warning was issued by Java, ignore this part of the type.
      }

      return result;
    }

    @Override
    public AnnotationMirrorMap<AnnotationMirror> visitIntersection_Intersection(
        AnnotatedIntersectionType type1, AnnotatedIntersectionType type2, Void aVoid) {
      AnnotationMirrorMap<AnnotationMirror> result = mapQualifierToPoly(type1, type2);
      return reduce(result, super.visitIntersection_Intersection(type1, type2, aVoid));
    }

    @Override
    public AnnotationMirrorMap<AnnotationMirror> visitNull_Null(
        AnnotatedNullType type1, AnnotatedNullType type2, Void aVoid) {
      return mapQualifierToPoly(type1, type2);
    }

    @Override
    public AnnotationMirrorMap<AnnotationMirror> visitPrimitive_Primitive(
        AnnotatedPrimitiveType type1, AnnotatedPrimitiveType type2, Void aVoid) {
      return mapQualifierToPoly(type1, type2);
    }

    @Override
    public AnnotationMirrorMap<AnnotationMirror> visitTypevar_Typevar(
        AnnotatedTypeVariable type1, AnnotatedTypeVariable type2, Void aVoid) {
      if (visited(type2)) {
        return new AnnotationMirrorMap<>();
      }
      AnnotationMirrorMap<AnnotationMirror> result = mapQualifierToPoly(type1, type2);
      return reduce(result, super.visitTypevar_Typevar(type1, type2, aVoid));
    }

    @Override
    public AnnotationMirrorMap<AnnotationMirror> visitUnion_Union(
        AnnotatedUnionType type1, AnnotatedUnionType type2, Void aVoid) {
      AnnotationMirrorMap<AnnotationMirror> result = mapQualifierToPoly(type1, type2);
      return reduce(result, super.visitUnion_Union(type1, type2, aVoid));
    }

    @Override
    public AnnotationMirrorMap<AnnotationMirror> visitWildcard_Wildcard(
        AnnotatedWildcardType type1, AnnotatedWildcardType type2, Void aVoid) {
      if (visited(type2)) {
        return new AnnotationMirrorMap<>();
      }
      AnnotationMirrorMap<AnnotationMirror> result = mapQualifierToPoly(type1, type2);
      return reduce(result, super.visitWildcard_Wildcard(type1, type2, aVoid));
    }

    /** Resets the state. */
    public void reset() {
      this.visitedTypes.clear();
      this.visited.clear();
    }
  }
}
