package org.checkerframework.framework.util;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.plumelib.util.IPair;

/**
 * Records any mapping between the type parameters of a subtype to the corresponding type parameters
 * of a supertype. For example, suppose we have the following classes:
 *
 * <pre>{@code
 * class Map<M1,M2>
 * class HashMap<H1, H2> extends Map<H1,H2>
 * }</pre>
 *
 * And we pass HashMap and Map to mapTypeArguments, the result would be:
 *
 * <pre>{@code
 * Map(H1 => M1, H2 => M2)
 * }</pre>
 *
 * Note, a single type argument in the subtype can map to multiple type parameters in the supertype.
 * e.g.,
 *
 * <pre>{@code
 * class OneTypeMap<O1> extends Map<O1,O1>
 * }</pre>
 *
 * would have the result:
 *
 * <pre>{@code
 * Map(O1 => [M1,M2])
 * }</pre>
 *
 * This utility only maps between corresponding type parameters, so the following class:
 *
 * <pre>{@code
 * class StringMap extends Map<String,String>
 * }</pre>
 *
 * would have an empty map as a result:
 *
 * <pre>{@code
 * Map() // there are no type argument relationships between the two types
 * }</pre>
 */
public class TypeArgumentMapper {

  /**
   * Returns a mapping from subtype's type parameter indices to the indices of corresponding type
   * parameters in supertype.
   */
  public static Set<IPair<Integer, Integer>> mapTypeArgumentIndices(
      TypeElement subtype, TypeElement supertype, Types types) {
    Set<IPair<Integer, Integer>> result = new HashSet<>();
    if (subtype.equals(supertype)) {
      for (int i = 0; i < subtype.getTypeParameters().size(); i++) {
        result.add(IPair.of(Integer.valueOf(i), Integer.valueOf(i)));
      }

    } else {
      Map<TypeParameterElement, Set<TypeParameterElement>> subToSuperElements =
          mapTypeArguments(subtype, supertype, types);
      Map<TypeParameterElement, Integer> supertypeIndexes = getElementToIndex(supertype);

      List<? extends TypeParameterElement> subtypeParams = subtype.getTypeParameters();
      for (int subtypeIndex = 0; subtypeIndex < subtypeParams.size(); subtypeIndex++) {
        TypeParameterElement subtypeParam = subtypeParams.get(subtypeIndex);

        Set<TypeParameterElement> correspondingSuperArgs = subToSuperElements.get(subtypeParam);
        if (correspondingSuperArgs != null) {
          for (TypeParameterElement supertypeParam : subToSuperElements.get(subtypeParam)) {
            result.add(IPair.of(subtypeIndex, supertypeIndexes.get(supertypeParam)));
          }
        }
      }
    }

    return result;
  }

  /**
   * Returns a Map(type parameter symbol &rarr; index in type parameter list).
   *
   * @param typeElement a type whose type parameters to summarize
   * @return a Map(type parameter symbol &rarr; index in type parameter list)
   */
  private static Map<TypeParameterElement, Integer> getElementToIndex(TypeElement typeElement) {
    Map<TypeParameterElement, Integer> result = new LinkedHashMap<>();

    List<? extends TypeParameterElement> params = typeElement.getTypeParameters();
    for (int i = 0; i < params.size(); i++) {
      result.put(params.get(i), Integer.valueOf(i));
    }

    return result;
  }

  /**
   * Returns a mapping from the type parameters of subtype to a list of the type parameters in
   * supertype that must be the same type as subtype.
   *
   * <p>e.g.,
   *
   * <pre>{@code
   * class A<A1,A2,A3>
   * class B<B1,B2,B3,B4> extends A<B1,B1,B3> {}
   * }</pre>
   *
   * results in a {@code Map(B1 => [A1,A2], B2 => [], B3 => [A3], B4 => [])}
   *
   * @return a mapping from the type parameters of subtype to the supertype type parameter's that to
   *     which they are a type argument
   */
  public static Map<TypeParameterElement, Set<TypeParameterElement>> mapTypeArguments(
      TypeElement subtype, TypeElement supertype, Types types) {

    List<TypeRecord> pathToSupertype = depthFirstSearchForSupertype(subtype, supertype, types);

    if (pathToSupertype == null || pathToSupertype.isEmpty()) {
      return new LinkedHashMap<>();
    }

    Map<TypeParameterElement, Set<TypeParameterElement>> intermediate = new LinkedHashMap<>();
    Set<TypeParameterElement> currentTypeParams = new HashSet<>();

    // takes a type records of the form:
    //  TypeRecord(element = MyMap<Y1,Y2>, type = null)
    //  TypeRecord(element = AbstractMap<A1,A2>, type = AbstractMap<Y1,Y2>)
    //  TypeRecord(element = Map<M1,M2>, type = AbstractMap<A1,A2>)
    // And makes a map:
    //   Map(Y1 -> [A1], Y2 -> [A2], A1 -> [M1], A2 -> M2]
    Iterator<TypeRecord> path = pathToSupertype.iterator();
    TypeRecord current = path.next();
    while (path.hasNext()) {
      TypeRecord next = path.next();

      List<? extends TypeParameterElement> nextTypeParameter = next.element.getTypeParameters();
      List<? extends TypeMirror> nextTypeArgs =
          next.type != null ? next.type.getTypeArguments() : Collections.emptyList();
      currentTypeParams.clear();
      currentTypeParams.addAll(current.element.getTypeParameters());

      for (int i = 0; i < nextTypeArgs.size(); i++) {
        TypeParameterElement correspondingParameter = nextTypeParameter.get(i);
        TypeMirror typeArg = nextTypeArgs.get(i);
        Element typeArgEle = types.asElement(typeArg);

        if (currentTypeParams.contains(typeArgEle)) {
          addToSetMap(intermediate, (TypeParameterElement) typeArgEle, correspondingParameter);
        }
      }
    }

    List<? extends TypeParameterElement> supertypeParams = supertype.getTypeParameters();
    Map<TypeParameterElement, Set<TypeParameterElement>> result =
        new LinkedHashMap<>(subtype.getTypeParameters().size());

    // You can think of the map above as a set of links from SubtypeParameter -> Supertype
    // Parameter
    for (TypeParameterElement subtypeParam : subtype.getTypeParameters()) {
      Set<TypeParameterElement> subtypePath =
          flattenPath(intermediate.get(subtypeParam), intermediate);
      subtypePath.retainAll(supertypeParams);
      result.put(subtypeParam, subtypePath);
    }

    return result;
  }

  private static Set<TypeParameterElement> flattenPath(
      Set<TypeParameterElement> elements,
      Map<TypeParameterElement, Set<TypeParameterElement>> map) {
    Set<TypeParameterElement> result = new HashSet<>();
    if (elements == null) {
      return result;
    }
    for (TypeParameterElement oldElement : elements) {
      Set<TypeParameterElement> substitutions = map.get(oldElement);
      if (substitutions != null) {
        result.addAll(flattenPath(elements, map));
      } else {
        result.add(oldElement);
      }
    }
    return result;
  }

  private static void addToSetMap(
      Map<TypeParameterElement, Set<TypeParameterElement>> setMap,
      TypeParameterElement element,
      TypeParameterElement typeParam) {
    Set<TypeParameterElement> set = setMap.computeIfAbsent(element, __ -> new HashSet<>());
    set.add(typeParam);
  }

  /**
   * Create a list of TypeRecord's that form a "path" to target from subtype. e.g. Suppose I have
   * the types
   *
   * <pre>{@code
   * interface Map<M1,M2>
   * class AbstractMap<A1,A2> implements Map<A1,A2>, Iterable<Map.Entry<M1,M2>>
   * class MyMap<Y1,Y2> extends AbstractMap<Y1,Y2> implements List<Map.Entry<Y1,Y2>>
   * }</pre>
   *
   * The path from MyMap to Map would be:
   *
   * <pre>{@code
   * TypeRecord(element = MyMap<Y1,Y2>, type = null)
   * TypeRecord(element = AbstractMap<A1,A2>, type = AbstractMap<Y1,Y2>)
   * TypeRecord(element = Map<M1,M2>, type = AbstractMap<A1,A2>)
   * }</pre>
   *
   * Note: You can have an implementation of the same interface inherited multiple times as long as
   * the parameterization of that interface remains the same e.g.
   *
   * <pre>{@code
   * interface List<E>
   * class AbstractList<A> implements List<E>
   * class ArrayList<T> extends AbstractList<T> implements List<T>
   * }</pre>
   *
   * Notice how ArrayList implements list both by inheriting from AbstractList and from explicitly
   * listing it in the implements clause. We prioritize finding a path through the list of
   * interfaces first since this will be the shorter path.
   *
   * @param subtype the start of the resulting sequence
   * @param target the end of the resulting sequence
   * @param types utility methods for operating on types
   * @return a list of type records that represents the sequence of directSupertypes between subtype
   *     and target
   */
  private static List<TypeRecord> depthFirstSearchForSupertype(
      TypeElement subtype, TypeElement target, Types types) {
    ArrayDeque<TypeRecord> pathFromRoot = new ArrayDeque<>();
    TypeRecord pathStart = new TypeRecord(subtype, null);
    pathFromRoot.push(pathStart);
    List<TypeRecord> result = recursiveDepthFirstSearch(pathFromRoot, target, types);
    return result;
  }

  /**
   * Computes one level for depthFirstSearchForSupertype then recurses.
   *
   * @param pathFromRoot the path so far
   * @param target the end of the resulting path
   * @param types utility methods for operating on types
   * @return a list of type records that extends pathFromRoot (a sequence of directSupertypes) to
   *     target
   */
  private static @Nullable List<TypeRecord> recursiveDepthFirstSearch(
      ArrayDeque<TypeRecord> pathFromRoot, TypeElement target, Types types) {
    if (pathFromRoot.isEmpty()) {
      return null;
    }

    TypeRecord currentRecord = pathFromRoot.peekLast();
    TypeElement currentElement = currentRecord.element;

    if (currentElement.equals(target)) {
      return new ArrayList<>(pathFromRoot);
    }

    Iterator<? extends TypeMirror> interfaces = currentElement.getInterfaces().iterator();
    TypeMirror superclassType = currentElement.getSuperclass();

    List<TypeRecord> path = null;

    while (path == null && interfaces.hasNext()) {
      TypeMirror intface = interfaces.next();
      if (intface.getKind() != TypeKind.NONE) {
        DeclaredType interfaceDeclared = (DeclaredType) intface;
        pathFromRoot.addLast(
            new TypeRecord((TypeElement) types.asElement(interfaceDeclared), interfaceDeclared));
        path = recursiveDepthFirstSearch(pathFromRoot, target, types);
        pathFromRoot.removeLast();
      }
    }

    if (path == null && superclassType.getKind() != TypeKind.NONE) {
      DeclaredType superclass = (DeclaredType) superclassType;

      pathFromRoot.addLast(new TypeRecord((TypeElement) types.asElement(superclass), superclass));
      path = recursiveDepthFirstSearch(pathFromRoot, target, types);
      pathFromRoot.removeLast();
    }

    return path;
  }

  /**
   * Maps a class or interface's declaration element to the type it would be if viewed from a
   * subtype class or interface.
   *
   * <p>e.g. suppose we have the elements for the declarations:
   *
   * <pre>{@code
   * class A<Ta>
   * class B<Tb> extends A<Tb>
   * }</pre>
   *
   * The type record of B if it is viewed as class A would bed:
   *
   * <pre>{@code
   * TypeRecord( element = A<Ta>, type = A<Tb> )
   * }</pre>
   *
   * That is, B can be viewed as an object of type A with an type argument of type parameter Tb
   */
  private static class TypeRecord {
    public final TypeElement element;
    public final DeclaredType type;

    TypeRecord(TypeElement element, DeclaredType type) {
      this.element = element;
      this.type = type;
    }

    @Override
    public String toString() {
      return String.format("[%s => %s]", element, type);
    }
  }
}
