package org.checkerframework.checker.index;

import com.sun.source.tree.Tree;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.VariableElement;
import org.checkerframework.checker.index.qual.HasSubsequence;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.dataflow.expression.FieldAccess;
import org.checkerframework.dataflow.expression.JavaExpression;
import org.checkerframework.framework.source.SourceChecker;
import org.checkerframework.framework.util.JavaExpressionParseUtil;
import org.checkerframework.framework.util.JavaExpressionParseUtil.JavaExpressionParseException;
import org.checkerframework.framework.util.StringToJavaExpression;
import org.checkerframework.javacutil.TreeUtils;

/** Holds information from {@link HasSubsequence} annotations. */
public class Subsequence {

  /** Name of the Subsequence. */
  public final String array;

  /** First index of the subsequence in the backing sequence. */
  public final String from;

  /** Last index of the subsequence in the backing sequence. */
  public final String to;

  private Subsequence(String array, String from, String to) {
    this.array = array;
    this.from = from;
    this.to = to;
  }

  /**
   * Returns a Subsequence representing the {@link HasSubsequence} annotation on the declaration of
   * {@code varTree} or null if there is not such annotation.
   *
   * <p>Note that this method does not standardize or viewpoint adapt the arguments to the
   * annotation, unlike getSubsequenceFromReceiver.
   *
   * @param varTree some tree
   * @param factory an AnnotatedTypeFactory
   * @return null or a new Subsequence from the declaration of {@code varTree}
   */
  public static @Nullable Subsequence getSubsequenceFromTree(
      Tree varTree, BaseAnnotatedTypeFactoryForIndexChecker factory) {

    if (!(varTree.getKind() == Tree.Kind.IDENTIFIER
        || varTree.getKind() == Tree.Kind.MEMBER_SELECT
        || varTree.getKind() == Tree.Kind.VARIABLE)) {
      return null;
    }

    Element element = TreeUtils.elementFromTree(varTree);
    AnnotationMirror hasSub = factory.getDeclAnnotation(element, HasSubsequence.class);
    return createSubsequence(hasSub, factory);
  }

  /**
   * Factory method to create a representation of a subsequence.
   *
   * @param hasSub {@link HasSubsequence} annotation or null
   * @param factory the type factory
   * @return a new Subsequence object representing {@code hasSub} or null
   */
  private static @Nullable Subsequence createSubsequence(
      AnnotationMirror hasSub, BaseAnnotatedTypeFactoryForIndexChecker factory) {
    if (hasSub == null) {
      return null;
    }
    String from = factory.hasSubsequenceFromValue(hasSub);
    String to = factory.hasSubsequenceToValue(hasSub);
    String array = factory.hasSubsequenceSubsequenceValue(hasSub);

    return new Subsequence(array, from, to);
  }

  /**
   * Returns a Subsequence representing the {@link HasSubsequence} annotation on the declaration of
   * {@code rec} or null if there is not such annotation.
   *
   * @param expr some tree
   * @param factory an AnnotatedTypeFactory
   * @return null or a new Subsequence from the declaration of {@code varTree}
   */
  public static @Nullable Subsequence getSubsequenceFromReceiver(
      JavaExpression expr, BaseAnnotatedTypeFactoryForIndexChecker factory) {
    if (!(expr instanceof FieldAccess)) {
      return null;
    }

    FieldAccess fa = (FieldAccess) expr;
    VariableElement element = fa.getField();
    AnnotationMirror hasSub = factory.getDeclAnnotation(element, HasSubsequence.class);
    if (hasSub == null) {
      return null;
    }

    String array =
        standardizeAndViewpointAdapt(
            factory.hasSubsequenceSubsequenceValue(hasSub), fa, factory.getChecker());
    String from =
        standardizeAndViewpointAdapt(
            factory.hasSubsequenceFromValue(hasSub), fa, factory.getChecker());
    String to =
        standardizeAndViewpointAdapt(
            factory.hasSubsequenceToValue(hasSub), fa, factory.getChecker());

    return new Subsequence(array, from, to);
  }

  /**
   * Helper function to standardize and viewpoint-adapt a string at a given field access. Wraps
   * {@link JavaExpressionParseUtil#parse}. If a parse exception is encountered, this returns its
   * argument.
   *
   * @param s a Java expression string
   * @param fieldAccess the field access
   * @param checker used to parse the expression
   * @return the argument, standardized and viewpoint-adapted
   */
  private static String standardizeAndViewpointAdapt(
      String s, FieldAccess fieldAccess, SourceChecker checker) {
    JavaExpression parseResult;
    try {
      parseResult = StringToJavaExpression.atFieldDecl(s, fieldAccess.getField(), checker);
    } catch (JavaExpressionParseException e) {
      return s;
    }

    return parseResult.atFieldAccess(fieldAccess.getReceiver()).toString();
  }

  /**
   * Returns the additive inverse of the given String. That is, if the result of this method is some
   * String s', then s + s' == 0 will evaluate to true. Note that this relies on the fact that the
   * JavaExpression parser cannot parse multiplication, so it naively just changes '-' to '+' and
   * vice-versa.
   *
   * @param s a Java expression string
   * @return the negated string
   */
  public static String negateString(String s) {
    String original = s;
    String result = "";
    if (!original.startsWith("-")) {
      result += '-';
    }
    for (int i = 0; i < original.length(); i++) {
      char c = original.charAt(i);
      if (c == '-') {
        result += '+';
      } else if (c == '+') {
        result += '-';
      } else {
        result += c;
      }
    }
    return result;
  }

  @Override
  public String toString() {
    return "Subsequence(array=" + this.array + ", from=" + this.from + ", to=" + this.to + ")";
  }
}
