package org.checkerframework.checker.index.samelen;

import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.util.Elements;
import org.checkerframework.checker.index.IndexMethodIdentifier;
import org.checkerframework.checker.index.IndexUtil;
import org.checkerframework.checker.index.qual.PolyLength;
import org.checkerframework.checker.index.qual.PolySameLen;
import org.checkerframework.checker.index.qual.SameLen;
import org.checkerframework.checker.index.qual.SameLenBottom;
import org.checkerframework.checker.index.qual.SameLenUnknown;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.dataflow.expression.ArrayCreation;
import org.checkerframework.dataflow.expression.ClassName;
import org.checkerframework.dataflow.expression.JavaExpression;
import org.checkerframework.dataflow.expression.ValueLiteral;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.ElementQualifierHierarchy;
import org.checkerframework.framework.type.QualifierHierarchy;
import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator;
import org.checkerframework.framework.type.treeannotator.TreeAnnotator;
import org.checkerframework.framework.util.JavaExpressionParseUtil;
import org.checkerframework.javacutil.AnnotationBuilder;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.TreeUtils;

/**
 * The SameLen Checker is used to determine whether there are multiple fixed-length sequences (such
 * as arrays or strings) in a program that share the same length. It is part of the Index Checker,
 * and is used as a subchecker by the Index Checker's components.
 *
 * <p>This type factory adds extra expressions to @SameLen annotations when necessary. For example,
 * if the full version of the type {@code @SameLen({"a","b"})} should include "a", "b", and whatever
 * is in the @SameLen types for "a" and for "b".
 *
 * <p>Also, every sequence s should have type @SameLen("s"). However, sometimes the sequence has
 * no @SameLen annotation, and users may write the annotation without the variable itself, as in
 *
 * <pre>{@code @SameLen("b") String a;}</pre>
 *
 * rather than the more pedantic
 *
 * <pre>{@code @SameLen({"a", "b"}) String a;}</pre>
 *
 * <p>Here are two specific examples where this annotated type factory refines types:
 *
 * <ul>
 *   <li>User-written SameLen: If a variable is declared with a user-written {@code @SameLen}
 *       annotation, then the type of the new variable is the union of the user-written arrays in
 *       the annotation and the arrays listed in the SameLen types of each of those arrays.
 *   <li>New array: The type of an expression of the form {@code new T[a.length]} is the union of
 *       the SameLen type of {@code a} and the arrays listed in {@code a}'s SameLen type.
 * </ul>
 */
public class SameLenAnnotatedTypeFactory extends BaseAnnotatedTypeFactory {

  /** The @{@link SameLenUnknown} annotation. */
  public final AnnotationMirror UNKNOWN =
      AnnotationBuilder.fromClass(elements, SameLenUnknown.class);

  /** The @{@link SameLenBottom} annotation. */
  private final AnnotationMirror BOTTOM =
      AnnotationBuilder.fromClass(elements, SameLenBottom.class);

  /** The @{@link PolySameLen} annotation. */
  private final AnnotationMirror POLY = AnnotationBuilder.fromClass(elements, PolySameLen.class);

  /** The SameLen.value field/element. */
  final ExecutableElement sameLenValueElement =
      TreeUtils.getMethod(SameLen.class, "value", 0, processingEnv);

  /** Predicates about method calls. */
  private final IndexMethodIdentifier imf = new IndexMethodIdentifier(this);

  /** Create a new SameLenAnnotatedTypeFactory. */
  public SameLenAnnotatedTypeFactory(BaseTypeChecker checker) {
    super(checker);

    addAliasedTypeAnnotation(PolyLength.class, POLY);

    this.postInit();
  }

  /** Gets a helper object that holds references to methods with special handling. */
  IndexMethodIdentifier getMethodIdentifier() {
    return imf;
  }

  @Override
  protected Set<Class<? extends Annotation>> createSupportedTypeQualifiers() {
    // Because the Index Checker is a subclass, the qualifiers have to be explicitly defined.
    return new LinkedHashSet<>(
        Arrays.asList(SameLen.class, SameLenBottom.class, SameLenUnknown.class, PolySameLen.class));
  }

  @Override
  protected QualifierHierarchy createQualifierHierarchy() {
    return new SameLenQualifierHierarchy(this.getSupportedTypeQualifiers(), elements);
  }

  // Handles case "user-written SameLen"
  @Override
  public AnnotatedTypeMirror getAnnotatedTypeLhs(Tree tree) {
    AnnotatedTypeMirror atm = super.getAnnotatedTypeLhs(tree);

    if (tree.getKind() == Tree.Kind.VARIABLE) {
      AnnotationMirror sameLenAnno = atm.getPrimaryAnnotation(SameLen.class);
      if (sameLenAnno != null) {
        JavaExpression je = JavaExpression.fromVariableTree((VariableTree) tree);
        String varName = je.toString();

        List<String> exprs =
            AnnotationUtils.getElementValueArray(sameLenAnno, sameLenValueElement, String.class);
        exprs.remove(varName);
        if (exprs.isEmpty()) {
          atm.replaceAnnotation(UNKNOWN);
        } else {
          atm.replaceAnnotation(createSameLen(exprs));
        }
      }
    }

    return atm;
  }

  /** Returns true if the given expression may appear in a @SameLen annotation. */
  public static boolean mayAppearInSameLen(JavaExpression expr) {
    return !expr.containsUnknown()
        && !(expr instanceof ArrayCreation)
        && !(expr instanceof ClassName)
        // avoid SameLen expressions with e.g. literal String constants
        && !(expr instanceof ValueLiteral)
        // Big expressions cause a stack overflow in JavaExpressionParseUtil.
        // So limit them to an arbitrary length of 999.
        && expr.toString().length() < 1000;
  }

  /**
   * The qualifier hierarchy for the SameLen type system. Most types are distinct and at the same
   * level: for instance @SameLen("a") and @SameLen("b) have nothing in common. However, if one type
   * includes even one overlapping name, then the types have to be the same:
   * so @SameLen({"a","b","c"} and @SameLen({"c","f","g"} are actually the same type -- both should
   * usually be replaced by a SameLen with the union of the lists of names.
   */
  private final class SameLenQualifierHierarchy extends ElementQualifierHierarchy {

    /**
     * Creates a SameLenQualifierHierarchy from the given classes.
     *
     * @param qualifierClasses classes of annotations that are the qualifiers
     * @param elements element utils
     */
    public SameLenQualifierHierarchy(
        Set<Class<? extends Annotation>> qualifierClasses, Elements elements) {
      super(qualifierClasses, elements, SameLenAnnotatedTypeFactory.this);
    }

    @Override
    public AnnotationMirror getTopAnnotation(AnnotationMirror start) {
      return UNKNOWN;
    }

    /**
     * If the collections are both non-empty and disjoint, returns null. Otherwise, returns their
     * union. The collections must not contain duplicates.
     *
     * @param c1 a collection of Strings (intended to be the value argument of a SameLen annotation)
     * @param c2 another collection of Strings
     * @return if the two inputs are disjoint (i.e., have no elements in common) and both are
     *     non-empty, returns null. Otherwise, returns the union of the two collections (which, if
     *     one collection is empty, is just the other collection). The result is sorted.
     */
    private @Nullable Collection<String> unionIfNotDisjoint(
        Collection<String> c1, Collection<String> c2) {
      if (c1.isEmpty()) {
        return c2;
      } else if (c2.isEmpty()) {
        return c1;
      }
      Set<String> result = new TreeSet<>(c1);
      boolean disjoint = true;
      for (String s : c2) {
        if (!result.add(s)) {
          disjoint = false;
        }
      }
      if (!disjoint) {
        return result;
      } else {
        return null;
      }
    }

    // The GLB of two SameLen annotations is the union of the two sets of arrays, or is bottom
    // if the sets do not intersect.
    @Override
    public AnnotationMirror greatestLowerBoundQualifiers(AnnotationMirror a1, AnnotationMirror a2) {
      if (areSameByClass(a1, SameLen.class) && areSameByClass(a2, SameLen.class)) {
        List<String> a1Val =
            AnnotationUtils.getElementValueArray(a1, sameLenValueElement, String.class);
        List<String> a2Val =
            AnnotationUtils.getElementValueArray(a2, sameLenValueElement, String.class);

        Collection<String> exprs = unionIfNotDisjoint(a1Val, a2Val);
        if (exprs == null) {
          return BOTTOM;
        } else {
          return createSameLen(exprs);
        }
      } else {
        // If one of the annotations is top, the glb is the other annotation; otherwise
        // bottom.
        if (areSameByClass(a1, SameLenUnknown.class)) {
          return a2;
        } else if (areSameByClass(a2, SameLenUnknown.class)) {
          return a1;
        } else {
          return BOTTOM;
        }
      }
    }

    // The LUB of two SameLen annotations is the intersection of the two sets of arrays, or is
    // top if they do not intersect.
    @Override
    public AnnotationMirror leastUpperBoundQualifiers(AnnotationMirror a1, AnnotationMirror a2) {
      if (areSameByClass(a1, SameLen.class) && areSameByClass(a2, SameLen.class)) {
        List<String> a1Val =
            AnnotationUtils.getElementValueArray(a1, sameLenValueElement, String.class);
        List<String> a2Val =
            AnnotationUtils.getElementValueArray(a2, sameLenValueElement, String.class);

        if (!Collections.disjoint(a1Val, a2Val)) {
          a1Val.retainAll(a2Val);
          return createSameLen(a1Val);
        } else {
          return UNKNOWN;
        }

      } else {
        // the lub is either one of the annotations (if the other is bottom), or top.
        if (areSameByClass(a1, SameLenBottom.class)) {
          return a2;
        } else if (areSameByClass(a2, SameLenBottom.class)) {
          return a1;
        } else if (areSameByClass(a1, PolySameLen.class) && areSameByClass(a2, PolySameLen.class)) {
          return a1;
        } else {
          return UNKNOWN;
        }
      }
    }

    @Override
    public boolean isSubtypeQualifiers(AnnotationMirror subAnno, AnnotationMirror superAnno) {
      if (areSameByClass(subAnno, SameLenBottom.class)) {
        return true;
      } else if (areSameByClass(superAnno, SameLenUnknown.class)) {
        return true;
      } else if (areSameByClass(subAnno, PolySameLen.class)) {
        return areSameByClass(superAnno, PolySameLen.class);
      } else if (areSameByClass(subAnno, SameLen.class)
          && areSameByClass(superAnno, SameLen.class)) {
        List<String> subArrays =
            AnnotationUtils.getElementValueArray(subAnno, sameLenValueElement, String.class);
        List<String> superArrays =
            AnnotationUtils.getElementValueArray(superAnno, sameLenValueElement, String.class);

        if (subArrays.containsAll(superArrays)) {
          return true;
        }
      }
      return false;
    }
  }

  @Override
  public TreeAnnotator createTreeAnnotator() {
    return new ListTreeAnnotator(super.createTreeAnnotator(), new SameLenTreeAnnotator(this));
  }

  /**
   * SameLen needs a tree annotator in order to properly type the right side of assignments of new
   * arrays that are initialized with the length of another array.
   */
  protected class SameLenTreeAnnotator extends TreeAnnotator {

    public SameLenTreeAnnotator(SameLenAnnotatedTypeFactory factory) {
      super(factory);
    }

    // Case "new array" for "new T[a.length]"
    @Override
    public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) {
      if (tree.getDimensions().size() == 1) {
        Tree dimensionTree = tree.getDimensions().get(0);
        ExpressionTree sequenceTree =
            IndexUtil.getLengthSequenceTree(dimensionTree, imf, processingEnv);
        if (sequenceTree != null) {
          AnnotationMirror sequenceAnno =
              getAnnotatedType(sequenceTree).getPrimaryAnnotationInHierarchy(UNKNOWN);

          JavaExpression sequenceExpr = JavaExpression.fromTree(sequenceTree);
          if (mayAppearInSameLen(sequenceExpr)) {
            String recString = sequenceExpr.toString();
            if (areSameByClass(sequenceAnno, SameLenUnknown.class)) {
              sequenceAnno = createSameLen(Collections.singletonList(recString));
            } else if (areSameByClass(sequenceAnno, SameLen.class)) {
              // Add the sequence whose length is being used to the annotation.
              List<String> exprs =
                  AnnotationUtils.getElementValueArray(
                      sequenceAnno, sameLenValueElement, String.class);
              int index = Collections.binarySearch(exprs, recString);
              if (index < 0) {
                exprs.add(-index - 1, recString);
                sequenceAnno = createSameLen(exprs);
              }
            }
          }
          type.addAnnotation(sequenceAnno);
        }
      }
      return null;
    }
  }

  /**
   * Find all the sequences that are members of the SameLen annotation associated with the sequence
   * named in sequenceExpression along the current path.
   */
  public List<String> getSameLensFromString(
      String sequenceExpression, Tree tree, TreePath currentPath) {
    AnnotationMirror sameLenAnno;
    try {
      sameLenAnno =
          getAnnotationFromJavaExpressionString(
              sequenceExpression, tree, currentPath, SameLen.class);
    } catch (JavaExpressionParseUtil.JavaExpressionParseException e) {
      // ignore parse errors
      return Collections.emptyList();
    }
    if (sameLenAnno == null) {
      return Collections.emptyList();
    }
    return AnnotationUtils.getElementValueArray(sameLenAnno, sameLenValueElement, String.class);
  }

  //
  // Creating @SameLen annotations
  //

  /**
   * Creates a @SameLen annotation whose values are the given strings.
   *
   * @param exprs the values for the @SameLen annotation. This must be an <em>ordered</em>
   *     collection such as a list or TreeSet in which the strings are in alphabetical order.
   * @return a @SameLen annotation whose values are the given strings
   */
  public AnnotationMirror createSameLen(Collection<String> exprs) {
    AnnotationBuilder builder = new AnnotationBuilder(processingEnv, SameLen.class);
    String[] exprArray = exprs.toArray(new String[0]);
    builder.setValue("value", exprArray);
    return builder.build();
  }

  /**
   * Generates a SameLen that includes each expression, as well as everything in the annotations2,
   * if they are SameLen annotations.
   *
   * @param exprs a list of expressions representing arrays to be included in the combined
   *     annotation
   * @param annos a list of annotations
   * @return a combined SameLen annotation
   */
  public AnnotationMirror createCombinedSameLen(
      List<JavaExpression> exprs, List<AnnotationMirror> annos) {

    Set<String> strings = new TreeSet<>();
    for (JavaExpression expr : exprs) {
      if (mayAppearInSameLen(expr)) {
        strings.add(expr.toString());
      }
    }
    for (AnnotationMirror anno : annos) {
      if (areSameByClass(anno, SameLen.class)) {
        strings.addAll(
            AnnotationUtils.getElementValueArray(anno, sameLenValueElement, String.class));
      }
    }
    return createSameLen(strings);
  }
}
