package org.checkerframework.checker.index.inequality;

import java.util.Collections;
import java.util.List;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.type.TypeKind;
import org.checkerframework.checker.index.IndexAbstractTransfer;
import org.checkerframework.common.value.ValueAnnotatedTypeFactory;
import org.checkerframework.common.value.ValueCheckerUtils;
import org.checkerframework.dataflow.analysis.TransferInput;
import org.checkerframework.dataflow.analysis.TransferResult;
import org.checkerframework.dataflow.cfg.node.Node;
import org.checkerframework.dataflow.cfg.node.NumericalSubtractionNode;
import org.checkerframework.dataflow.expression.JavaExpression;
import org.checkerframework.dataflow.expression.ValueLiteral;
import org.checkerframework.framework.flow.CFAnalysis;
import org.checkerframework.framework.flow.CFStore;
import org.checkerframework.framework.flow.CFValue;
import org.checkerframework.framework.util.JavaExpressionParseUtil;
import org.checkerframework.javacutil.AnnotationMirrorSet;
import org.plumelib.util.CollectionsPlume;

/**
 * Implements 3 refinement rules:
 *
 * <ul>
 *   <li>1. if left &gt; right, right has type {@code @LessThan("left")}
 *   <li>2. if left &ge; right, right has type {@code @LessThan("left + 1")}
 *   <li>3. if {@code 0 < right}, {@code left - right} has type {@code @LessThan("left")}
 * </ul>
 *
 * These are implemented generally, so they also apply to e.g. &lt; and &le; comparisons.
 */
public class LessThanTransfer extends IndexAbstractTransfer {

  public LessThanTransfer(CFAnalysis analysis) {
    super(analysis);
  }

  /** Case 1. */
  @Override
  protected void refineGT(
      Node left,
      AnnotationMirror leftAnno,
      Node right,
      AnnotationMirror rightAnno,
      CFStore store,
      TransferInput<CFValue, CFStore> in) {
    // left > right so right < left
    // Refine right to @LessThan("left")
    JavaExpression leftJe = JavaExpression.fromNode(left);
    if (leftJe != null && !leftJe.isAssignableByOtherCode()) {
      if (isDoubleOrFloatLiteral(leftJe)) {
        return;
      }
      LessThanAnnotatedTypeFactory factory =
          (LessThanAnnotatedTypeFactory) analysis.getTypeFactory();
      List<String> lessThanExpressions = factory.getLessThanExpressions(rightAnno);
      if (lessThanExpressions == null) {
        // right is already bottom, nothing to refine.
        return;
      }
      String leftString = leftJe.toString();
      if (!lessThanExpressions.contains(leftString)) {
        lessThanExpressions = CollectionsPlume.append(lessThanExpressions, leftString);
        JavaExpression rightJe = JavaExpression.fromNode(right);
        store.insertValue(rightJe, factory.createLessThanQualifier(lessThanExpressions));
      }
    }
  }

  /** Case 2. */
  @Override
  protected void refineGTE(
      Node left,
      AnnotationMirror leftAnno,
      Node right,
      AnnotationMirror rightAnno,
      CFStore store,
      TransferInput<CFValue, CFStore> in) {
    // left >= right so right is less than left
    // Refine right to @LessThan("left + 1")

    // left > right so right is less than left
    // Refine right to @LessThan("left")
    JavaExpression leftJe = JavaExpression.fromNode(left);
    if (leftJe != null && !leftJe.isAssignableByOtherCode()) {
      if (isDoubleOrFloatLiteral(leftJe)) {
        return;
      }
      LessThanAnnotatedTypeFactory factory =
          (LessThanAnnotatedTypeFactory) analysis.getTypeFactory();
      List<String> lessThanExpressions = factory.getLessThanExpressions(rightAnno);
      if (lessThanExpressions == null) {
        // right is already bottom, nothing to refine.
        return;
      }
      String leftIncremented = incrementedExpression(leftJe);
      if (!lessThanExpressions.contains(leftIncremented)) {
        lessThanExpressions = CollectionsPlume.append(lessThanExpressions, leftIncremented);
        JavaExpression rightJe = JavaExpression.fromNode(right);
        store.insertValue(rightJe, factory.createLessThanQualifier(lessThanExpressions));
      }
    }
  }

  /** Case 3. */
  @Override
  public TransferResult<CFValue, CFStore> visitNumericalSubtraction(
      NumericalSubtractionNode n, TransferInput<CFValue, CFStore> in) {
    TransferResult<CFValue, CFStore> result = super.visitNumericalSubtraction(n, in);
    LessThanAnnotatedTypeFactory factory = (LessThanAnnotatedTypeFactory) analysis.getTypeFactory();
    JavaExpression leftJe = JavaExpression.fromNode(n.getLeftOperand());
    if (leftJe != null && !leftJe.isAssignableByOtherCode()) {
      ValueAnnotatedTypeFactory valueFactory = factory.getValueAnnotatedTypeFactory();
      Long right = ValueCheckerUtils.getMinValue(n.getRightOperand().getTree(), valueFactory);
      if (right != null && 0 < right) {
        // left - right < left iff 0 < right
        List<String> expressions = getLessThanExpressions(n.getLeftOperand());
        if (!isDoubleOrFloatLiteral(leftJe)) {
          if (expressions == null) {
            expressions = Collections.singletonList(leftJe.toString());
          } else {
            expressions = CollectionsPlume.append(expressions, leftJe.toString());
          }
        }
        AnnotationMirror refine = factory.createLessThanQualifier(expressions);
        return recreateTransferResult(refine, result);
      }
    }
    return result;
  }

  /**
   * Return the expressions that {@code node} is less than.
   *
   * @param node a CFG node
   * @return the expressions that {@code node} is less than
   */
  private List<String> getLessThanExpressions(Node node) {
    AnnotationMirrorSet s = analysis.getValue(node).getAnnotations();
    if (s != null && !s.isEmpty()) {
      LessThanAnnotatedTypeFactory factory =
          (LessThanAnnotatedTypeFactory) analysis.getTypeFactory();
      return factory.getLessThanExpressions(
          factory.getQualifierHierarchy().findAnnotationInHierarchy(s, factory.LESS_THAN_UNKNOWN));
    } else {
      return Collections.emptyList();
    }
  }

  /**
   * Return true if {@code expr} is a double or float literal, which can't be parsed by {@link
   * JavaExpressionParseUtil}.
   */
  private boolean isDoubleOrFloatLiteral(JavaExpression expr) {
    if (expr instanceof ValueLiteral) {
      return expr.getType().getKind() == TypeKind.DOUBLE
          || expr.getType().getKind() == TypeKind.FLOAT;
    } else {
      return false;
    }
  }

  /**
   * Return the string representation of {@code expr + 1}.
   *
   * @param expr a JavaExpression
   * @return the string representation of {@code expr + 1}
   */
  private String incrementedExpression(JavaExpression expr) {
    expr = ValueCheckerUtils.optimize(expr, analysis.getTypeFactory());
    if (expr instanceof ValueLiteral) {
      ValueLiteral literal = (ValueLiteral) expr;
      if (literal.getValue() instanceof Number) {
        long longLiteral = ((Number) literal.getValue()).longValue();
        if (longLiteral != Long.MAX_VALUE) {
          return (longLiteral + 1) + "L";
        }
      }
    }

    // Could do more optimization to merge with a literal at end of `exprString`.  Is that
    // needed?
    return expr + " + 1";
  }
}
