package org.checkerframework.common.value;

import com.sun.source.tree.ExpressionTree;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.common.value.qual.ArrayLen;
import org.checkerframework.common.value.qual.ArrayLenRange;
import org.checkerframework.common.value.qual.StringVal;
import org.checkerframework.common.value.util.NumberMath;
import org.checkerframework.common.value.util.NumberUtils;
import org.checkerframework.common.value.util.Range;
import org.checkerframework.dataflow.analysis.ConditionalTransferResult;
import org.checkerframework.dataflow.analysis.RegularTransferResult;
import org.checkerframework.dataflow.analysis.TransferInput;
import org.checkerframework.dataflow.analysis.TransferResult;
import org.checkerframework.dataflow.cfg.node.BitwiseAndNode;
import org.checkerframework.dataflow.cfg.node.BitwiseComplementNode;
import org.checkerframework.dataflow.cfg.node.BitwiseOrNode;
import org.checkerframework.dataflow.cfg.node.BitwiseXorNode;
import org.checkerframework.dataflow.cfg.node.ConditionalAndNode;
import org.checkerframework.dataflow.cfg.node.ConditionalNotNode;
import org.checkerframework.dataflow.cfg.node.ConditionalOrNode;
import org.checkerframework.dataflow.cfg.node.EqualToNode;
import org.checkerframework.dataflow.cfg.node.FieldAccessNode;
import org.checkerframework.dataflow.cfg.node.FloatingDivisionNode;
import org.checkerframework.dataflow.cfg.node.FloatingRemainderNode;
import org.checkerframework.dataflow.cfg.node.GreaterThanNode;
import org.checkerframework.dataflow.cfg.node.GreaterThanOrEqualNode;
import org.checkerframework.dataflow.cfg.node.IntegerDivisionNode;
import org.checkerframework.dataflow.cfg.node.IntegerRemainderNode;
import org.checkerframework.dataflow.cfg.node.LeftShiftNode;
import org.checkerframework.dataflow.cfg.node.LessThanNode;
import org.checkerframework.dataflow.cfg.node.LessThanOrEqualNode;
import org.checkerframework.dataflow.cfg.node.MethodAccessNode;
import org.checkerframework.dataflow.cfg.node.MethodInvocationNode;
import org.checkerframework.dataflow.cfg.node.Node;
import org.checkerframework.dataflow.cfg.node.NotEqualNode;
import org.checkerframework.dataflow.cfg.node.NumericalAdditionNode;
import org.checkerframework.dataflow.cfg.node.NumericalMinusNode;
import org.checkerframework.dataflow.cfg.node.NumericalMultiplicationNode;
import org.checkerframework.dataflow.cfg.node.NumericalPlusNode;
import org.checkerframework.dataflow.cfg.node.NumericalSubtractionNode;
import org.checkerframework.dataflow.cfg.node.SignedRightShiftNode;
import org.checkerframework.dataflow.cfg.node.StringConcatenateNode;
import org.checkerframework.dataflow.cfg.node.StringConversionNode;
import org.checkerframework.dataflow.cfg.node.StringLiteralNode;
import org.checkerframework.dataflow.cfg.node.UnsignedRightShiftNode;
import org.checkerframework.dataflow.expression.JavaExpression;
import org.checkerframework.dataflow.expression.Unknown;
import org.checkerframework.dataflow.util.NodeUtils;
import org.checkerframework.framework.flow.CFAbstractAnalysis;
import org.checkerframework.framework.flow.CFAbstractStore;
import org.checkerframework.framework.flow.CFStore;
import org.checkerframework.framework.flow.CFTransfer;
import org.checkerframework.framework.flow.CFValue;
import org.checkerframework.framework.type.QualifierHierarchy;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.TreeUtils;
import org.checkerframework.javacutil.TypeSystemError;
import org.checkerframework.javacutil.TypesUtils;
import org.plumelib.util.CollectionsPlume;

/** The transfer class for the Value Checker. */
public class ValueTransfer extends CFTransfer {

  /** The Value type factory. */
  protected final ValueAnnotatedTypeFactory atypeFactory;

  /** The Value qualifier hierarchy. */
  protected final QualifierHierarchy qualHierarchy;

  /** True if -AnonNullStringsConcatenation was passed on the command line. */
  private final boolean nonNullStringsConcatenation;

  /**
   * Create a new ValueTransfer.
   *
   * @param analysis the corresponding analysis
   */
  public ValueTransfer(CFAbstractAnalysis<CFValue, CFStore, CFTransfer> analysis) {
    super(analysis);
    atypeFactory = (ValueAnnotatedTypeFactory) analysis.getTypeFactory();
    qualHierarchy = atypeFactory.getQualifierHierarchy();
    nonNullStringsConcatenation =
        atypeFactory.getChecker().hasOption("nonNullStringsConcatenation");
  }

  /** Returns a range of possible lengths for an integer from a range, as casted to a String. */
  private Range getIntRangeStringLengthRange(Node subNode, TransferInput<CFValue, CFStore> p) {
    Range valueRange = getIntRange(subNode, p);

    // Get lengths of the bounds
    int fromLength = Long.toString(valueRange.from).length();
    int toLength = Long.toString(valueRange.to).length();

    int lowerLength = Math.min(fromLength, toLength);
    // In case the range contains 0, the minimum length is 1 even if both bounds are longer
    if (valueRange.contains(0)) {
      lowerLength = 1;
    }

    int upperLength = Math.max(fromLength, toLength);

    return Range.create(lowerLength, upperLength);
  }

  /**
   * Returns a range of possible lengths for {@code subNode}, as casted to a String.
   *
   * @param subNode some subnode of {@code p}
   * @param p a TransferInput
   * @return a range of possible lengths for {@code subNode}, as casted to a String
   */
  private @Nullable Range getStringLengthRange(Node subNode, TransferInput<CFValue, CFStore> p) {
    CFValue value = p.getValueOfSubNode(subNode);

    AnnotationMirror anno = getValueAnnotation(value);
    if (anno == null) {
      return null;
    }
    String annoName = AnnotationUtils.annotationName(anno);
    if (annoName.equals(ValueAnnotatedTypeFactory.ARRAYLENRANGE_NAME)) {
      return atypeFactory.getRange(anno);
    } else if (annoName.equals(ValueAnnotatedTypeFactory.BOTTOMVAL_NAME)) {
      return Range.NOTHING;
    }

    TypeKind subNodeTypeKind = subNode.getType().getKind();

    // handle values converted to string (ints, longs, longs with @IntRange)
    if (subNode instanceof StringConversionNode) {
      return getStringLengthRange(((StringConversionNode) subNode).getOperand(), p);
    } else if (isIntRange(subNode, p)) {
      return getIntRangeStringLengthRange(subNode, p);
    } else if (subNodeTypeKind == TypeKind.INT) {
      // ints are between 1 and 11 characters long
      return Range.create(1, 11);
    } else if (subNodeTypeKind == TypeKind.LONG) {
      // longs are between 1 and 20 characters long
      return Range.create(1, 20);
    }

    return Range.create(0, Integer.MAX_VALUE);
  }

  /**
   * Returns a list of possible lengths for {@code subNode}, as casted to a String. Returns null if
   * {@code subNode}'s type is top/unknown. Returns an empty list if {@code subNode}'s type is
   * bottom.
   */
  private @Nullable List<Integer> getStringLengths(
      Node subNode, TransferInput<CFValue, CFStore> p) {

    CFValue value = p.getValueOfSubNode(subNode);
    AnnotationMirror anno = getValueAnnotation(value);
    if (anno == null) {
      return null;
    }
    String annoName = AnnotationUtils.annotationName(anno);
    if (annoName.equals(ValueAnnotatedTypeFactory.ARRAYLEN_NAME)) {
      return atypeFactory.getArrayLength(anno);
    } else if (annoName.equals(ValueAnnotatedTypeFactory.BOTTOMVAL_NAME)) {
      return Collections.emptyList();
    }

    TypeKind subNodeTypeKind = subNode.getType().getKind();

    // handle values converted to string (characters, bytes, shorts, ints with @IntRange)
    if (subNode instanceof StringConversionNode) {
      return getStringLengths(((StringConversionNode) subNode).getOperand(), p);
    } else if (subNodeTypeKind == TypeKind.CHAR) {
      // characters always have length 1
      return Collections.singletonList(1);
    } else if (isIntRange(subNode, p)) {
      // Try to get a list of lengths from a range of integer values converted to string.
      // @IntVal is not checked for, because if it is present, we would already have the
      // actual string values.
      Range lengthRange = getIntRangeStringLengthRange(subNode, p);
      return ValueCheckerUtils.getValuesFromRange(lengthRange, Integer.class);
    } else if (subNodeTypeKind == TypeKind.BYTE) {
      // bytes are between 1 and 4 characters long
      return ValueCheckerUtils.getValuesFromRange(Range.create(1, 4), Integer.class);
    } else if (subNodeTypeKind == TypeKind.SHORT) {
      // shorts are between 1 and 6 characters long
      return ValueCheckerUtils.getValuesFromRange(Range.create(1, 6), Integer.class);
    } else {
      return null;
    }
  }

  /**
   * Returns a list of possible values for {@code subNode}, as casted to a String. Returns null if
   * {@code subNode}'s type is top/unknown. Returns an empty list if {@code subNode}'s type is
   * bottom.
   *
   * @param subNode a subNode of p
   * @param p a TransferInput
   * @return a list of possible values for {@code subNode} or null
   */
  private @Nullable List<String> getStringValues(Node subNode, TransferInput<CFValue, CFStore> p) {
    CFValue value = p.getValueOfSubNode(subNode);
    AnnotationMirror anno = getValueAnnotation(value);
    if (anno == null) {
      return null;
    }
    String annoName = AnnotationUtils.annotationName(anno);
    switch (annoName) {
      case ValueAnnotatedTypeFactory.UNKNOWN_NAME:
        return null;
      case ValueAnnotatedTypeFactory.BOTTOMVAL_NAME:
        return Collections.emptyList();
      case ValueAnnotatedTypeFactory.STRINGVAL_NAME:
        return atypeFactory.getStringValues(anno);
      default:
        // Do nothing.
    }

    // @IntVal, @IntRange, @DoubleVal, @BoolVal (have to be converted to string)
    List<? extends Object> values;
    if (annoName.equals(ValueAnnotatedTypeFactory.BOOLVAL_NAME)) {
      values = getBooleanValues(subNode, p);
    } else if (subNode.getType().getKind() == TypeKind.CHAR) {
      values = getCharValues(subNode, p);
    } else if (subNode instanceof StringConversionNode) {
      return getStringValues(((StringConversionNode) subNode).getOperand(), p);
    } else if (isIntRange(subNode, p)) {
      Range range = getIntRange(subNode, p);
      List<Long> longValues = ValueCheckerUtils.getValuesFromRange(range, Long.class);
      values = NumberUtils.castNumbers(subNode.getType(), longValues);
    } else {
      values = getNumericalValues(subNode, p);
    }
    if (values == null) {
      return null;
    }
    List<String> stringValues = CollectionsPlume.mapList(Object::toString, values);
    // Empty list means bottom value
    return stringValues.isEmpty() ? Collections.singletonList("null") : stringValues;
  }

  /**
   * Create a @BoolVal CFValue for the given boolean value.
   *
   * @param value the value for the @BoolVal annotation
   * @return a @BoolVal CFValue for the given boolean value
   */
  private CFValue createBooleanCFValue(boolean value) {
    return analysis.createSingleAnnotationValue(
        value ? atypeFactory.BOOLEAN_TRUE : atypeFactory.BOOLEAN_FALSE,
        atypeFactory.types.getPrimitiveType(TypeKind.BOOLEAN));
  }

  /**
   * Get the unique possible boolean value from @BoolVal. Returns null if that is not the case
   * (including if the CFValue is not @BoolVal).
   *
   * @param value a CFValue
   * @return theboolean if {@code value} represents a single boolean value; otherwise null
   */
  private @Nullable Boolean getBooleanValue(CFValue value) {
    AnnotationMirror boolAnno =
        AnnotationUtils.getAnnotationByName(
            value.getAnnotations(), ValueAnnotatedTypeFactory.BOOLVAL_NAME);
    return atypeFactory.getBooleanValue(boolAnno);
  }

  /**
   * Get possible boolean values for a node. Returns null if there is no estimate, because the
   * node's value is not @BoolVal.
   *
   * @param subNode the node whose value to obtain
   * @param p the transfer input in which to look up values
   * @return the possible boolean values for the node
   */
  private @Nullable List<Boolean> getBooleanValues(
      Node subNode, TransferInput<CFValue, CFStore> p) {
    CFValue value = p.getValueOfSubNode(subNode);
    AnnotationMirror intAnno =
        AnnotationUtils.getAnnotationByName(
            value.getAnnotations(), ValueAnnotatedTypeFactory.BOOLVAL_NAME);
    return atypeFactory.getBooleanValues(intAnno);
  }

  /** Get possible char values from annotation @IntRange or @IntVal. */
  private List<Character> getCharValues(Node subNode, TransferInput<CFValue, CFStore> p) {
    CFValue value = p.getValueOfSubNode(subNode);
    AnnotationMirror intAnno;

    intAnno =
        AnnotationUtils.getAnnotationByName(
            value.getAnnotations(), ValueAnnotatedTypeFactory.INTVAL_NAME);
    if (intAnno != null) {
      return atypeFactory.getCharValues(intAnno);
    }

    if (atypeFactory.isIntRange(value.getAnnotations())) {
      intAnno =
          qualHierarchy.findAnnotationInHierarchy(value.getAnnotations(), atypeFactory.UNKNOWNVAL);
      Range range = atypeFactory.getRange(intAnno);
      return ValueCheckerUtils.getValuesFromRange(range, Character.class);
    }

    return Collections.emptyList();
  }

  private AnnotationMirror getValueAnnotation(Node subNode, TransferInput<CFValue, CFStore> p) {
    CFValue value = p.getValueOfSubNode(subNode);
    return getValueAnnotation(value);
  }

  /**
   * Extract the Value Checker annotation from a CFValue object.
   *
   * @param cfValue a CFValue object
   * @return the Value Checker annotation within cfValue
   */
  private AnnotationMirror getValueAnnotation(CFValue cfValue) {
    return qualHierarchy.findAnnotationInHierarchy(
        cfValue.getAnnotations(), atypeFactory.UNKNOWNVAL);
  }

  /**
   * Returns a list of possible values, or null if no estimate is available and any value is
   * possible.
   */
  private @Nullable List<? extends Number> getNumericalValues(
      Node subNode, TransferInput<CFValue, CFStore> p) {
    AnnotationMirror valueAnno = getValueAnnotation(subNode, p);
    return getNumericalValues(subNode, valueAnno);
  }

  /**
   * Returns the numerical values in valueAnno casted to the type of subNode.
   *
   * @param subNode node
   * @param valueAnno annotation mirror
   * @return the numerical values in valueAnno casted to the type of subNode
   */
  private @Nullable List<? extends Number> getNumericalValues(
      Node subNode, AnnotationMirror valueAnno) {

    if (valueAnno == null
        || AnnotationUtils.areSameByName(valueAnno, ValueAnnotatedTypeFactory.UNKNOWN_NAME)) {
      return null;
    } else if (AnnotationUtils.areSameByName(valueAnno, ValueAnnotatedTypeFactory.BOTTOMVAL_NAME)) {
      return Collections.emptyList();
    }
    List<? extends Number> values;
    if (AnnotationUtils.areSameByName(valueAnno, ValueAnnotatedTypeFactory.INTVAL_NAME)) {
      values = atypeFactory.getIntValues(valueAnno);
    } else if (AnnotationUtils.areSameByName(valueAnno, ValueAnnotatedTypeFactory.DOUBLEVAL_NAME)) {
      values = atypeFactory.getDoubleValues(valueAnno);
    } else {
      return null;
    }
    return NumberUtils.castNumbers(subNode.getType(), values);
  }

  /** Get possible integer range from annotation. */
  private Range getIntRange(Node subNode, TransferInput<CFValue, CFStore> p) {
    AnnotationMirror val = getValueAnnotation(subNode, p);
    return getIntRangeFromAnnotation(subNode, val);
  }

  /**
   * Returns the {@link Range} object corresponding to the annotation {@code val} casted to the type
   * of {@code node}.
   *
   * @param node a node
   * @param val annotation mirror
   * @return the {@link Range} object corresponding to the annotation {@code val} casted to the type
   *     of {@code node}.
   */
  private Range getIntRangeFromAnnotation(Node node, AnnotationMirror val) {
    Range range;
    if (val == null || AnnotationUtils.areSameByName(val, ValueAnnotatedTypeFactory.UNKNOWN_NAME)) {
      range = Range.EVERYTHING;
    } else if (atypeFactory.isIntRange(val)) {
      range = atypeFactory.getRange(val);
    } else if (AnnotationUtils.areSameByName(val, ValueAnnotatedTypeFactory.INTVAL_NAME)) {
      List<Long> values = atypeFactory.getIntValues(val);
      range = ValueCheckerUtils.getRangeFromValues(values);
    } else if (AnnotationUtils.areSameByName(val, ValueAnnotatedTypeFactory.DOUBLEVAL_NAME)) {
      List<Double> values = atypeFactory.getDoubleValues(val);
      range = ValueCheckerUtils.getRangeFromValues(values);
    } else if (AnnotationUtils.areSameByName(val, ValueAnnotatedTypeFactory.BOTTOMVAL_NAME)) {
      return Range.NOTHING;
    } else {
      range = Range.EVERYTHING;
    }
    return NumberUtils.castRange(node.getType(), range);
  }

  /**
   * Returns true if subNode is annotated with {@code @IntRange}.
   *
   * @param subNode subNode of {@code p}
   * @param p a TransferInput
   * @return true if this subNode is annotated with {@code @IntRange}
   */
  private boolean isIntRange(Node subNode, TransferInput<CFValue, CFStore> p) {
    CFValue value = p.getValueOfSubNode(subNode);
    return atypeFactory.isIntRange(value.getAnnotations());
  }

  /**
   * Returns true if {@code node} an integral type and is {@code anno} is {@code @UnknownVal}.
   *
   * @param node a node
   * @param anno annotation mirror
   * @return true if node is annotated with {@code @UnknownVal} and it is an integral type
   */
  private boolean isIntegralUnknownVal(Node node, AnnotationMirror anno) {
    return AnnotationUtils.areSameByName(anno, ValueAnnotatedTypeFactory.UNKNOWN_NAME)
        && TypesUtils.isIntegralPrimitive(node.getType());
  }

  /**
   * Returns true if this node is annotated with {@code @IntRange} or {@code @UnknownVal}.
   *
   * @param node the node to inspect
   * @param p storage
   * @return true if this node is annotated with {@code @IntRange} or {@code @UnknownVal}
   */
  private boolean isIntRangeOrIntegralUnknownVal(Node node, TransferInput<CFValue, CFStore> p) {
    if (isIntRange(node, p)) {
      return true;
    }
    return isIntegralUnknownVal(node, getValueAnnotation(p.getValueOfSubNode(node)));
  }

  /**
   * Create a new boolean transfer result based on the original result and the new annotation.
   *
   * @param thenStore the then store for the result
   * @param elseStore the else store for the result
   * @param booleanValues the possible values that the result might evaluate to
   * @param underlyingType the (boolean) result type. This is always boolean; it is an argument
   *     because there is no easy way to get a TypeMirror for a specific type.
   * @return a transfer result like {@code result}, but permitting only the given boolean values
   */
  private TransferResult<CFValue, CFStore> createNewResultBoolean(
      CFStore thenStore,
      CFStore elseStore,
      List<Boolean> booleanValues,
      TypeMirror underlyingType) {
    AnnotationMirror boolVal = atypeFactory.createBooleanAnnotation(booleanValues);
    CFValue newResultValue = analysis.createSingleAnnotationValue(boolVal, underlyingType);
    if (elseStore != null) {
      return new ConditionalTransferResult<>(newResultValue, thenStore, elseStore);
    } else {
      return new RegularTransferResult<>(newResultValue, thenStore);
    }
  }

  @Override
  public TransferResult<CFValue, CFStore> visitFieldAccess(
      FieldAccessNode node, TransferInput<CFValue, CFStore> in) {

    TransferResult<CFValue, CFStore> result = super.visitFieldAccess(node, in);
    refineArrayAtLengthAccess(node, result.getRegularStore());
    return result;
  }

  @Override
  public TransferResult<CFValue, CFStore> visitMethodInvocation(
      MethodInvocationNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> result = super.visitMethodInvocation(n, p);
    refineAtLengthInvocation(n, result.getRegularStore());
    return result;
  }

  /**
   * If array.length is encountered, transform its @IntVal annotation into an @ArrayLen annotation
   * for array.
   */
  private void refineArrayAtLengthAccess(FieldAccessNode arrayLengthNode, CFStore store) {
    if (!NodeUtils.isArrayLengthFieldAccess(arrayLengthNode)) {
      return;
    }

    refineAtLengthAccess(arrayLengthNode, arrayLengthNode.getReceiver(), store);
  }

  /**
   * If length method is invoked for a sequence, transform its @IntVal annotation into an @ArrayLen
   * annotation.
   *
   * @param lengthNode the length method invocation node
   * @param store the Checker Framework store
   */
  private void refineAtLengthInvocation(MethodInvocationNode lengthNode, CFStore store) {
    if (atypeFactory
        .getMethodIdentifier()
        .isStringLengthMethod(lengthNode.getTarget().getMethod())) {
      MethodAccessNode methodAccessNode = lengthNode.getTarget();
      refineAtLengthAccess(lengthNode, methodAccessNode.getReceiver(), store);
    } else if (atypeFactory
        .getMethodIdentifier()
        .isArrayGetLengthMethod(lengthNode.getTarget().getMethod())) {
      Node node = lengthNode.getArguments().get(0);
      refineAtLengthAccess(lengthNode, node, store);
    }
  }

  /**
   * Gets a value checker annotation relevant for an array or a string.
   *
   * @param arrayOrStringNode the node whose annotation to return
   * @return the value checker annotation for the array or a string
   */
  private AnnotationMirror getArrayOrStringAnnotation(Node arrayOrStringNode) {
    AnnotationMirror arrayOrStringAnno =
        atypeFactory.getAnnotationMirror(arrayOrStringNode.getTree(), StringVal.class);
    if (arrayOrStringAnno == null) {
      arrayOrStringAnno =
          atypeFactory.getAnnotationMirror(arrayOrStringNode.getTree(), ArrayLen.class);
    }
    if (arrayOrStringAnno == null) {
      arrayOrStringAnno =
          atypeFactory.getAnnotationMirror(arrayOrStringNode.getTree(), ArrayLenRange.class);
    }

    return arrayOrStringAnno;
  }

  /**
   * Transform @IntVal or @IntRange annotations of an array or string length into an @ArrayLen
   * or @ArrayLenRange annotation for the array or string.
   *
   * @param lengthNode an invocation of method {@code length} or an access of the {@code length}
   *     field
   * @param receiverNode the receiver of {@code lengthNode}
   * @param store the store to update
   */
  private void refineAtLengthAccess(Node lengthNode, Node receiverNode, CFStore store) {
    JavaExpression lengthExpr = JavaExpression.fromNode(lengthNode);

    // If the expression is not representable (for example if String.length() for some reason is
    // not marked @Pure, then do not refine.
    if (lengthExpr instanceof Unknown) {
      return;
    }

    CFValue value = store.getValue(lengthExpr);
    if (value == null) {
      return;
    }

    AnnotationMirror lengthAnno = getValueAnnotation(value);
    if (lengthAnno == null) {
      return;
    }
    JavaExpression receiverJE = JavaExpression.fromNode(receiverNode);
    if (AnnotationUtils.areSameByName(lengthAnno, ValueAnnotatedTypeFactory.BOTTOMVAL_NAME)) {
      // If the length is bottom, then this is dead code, so the receiver type
      // should also be bottom.
      store.insertValue(receiverJE, lengthAnno);
      return;
    }

    RangeOrListOfValues rolv;
    if (atypeFactory.isIntRange(lengthAnno)) {
      rolv = new RangeOrListOfValues(atypeFactory.getRange(lengthAnno));
    } else if (AnnotationUtils.areSameByName(lengthAnno, ValueAnnotatedTypeFactory.INTVAL_NAME)) {
      List<Long> lengthValues = atypeFactory.getIntValues(lengthAnno);
      rolv = new RangeOrListOfValues(RangeOrListOfValues.convertLongsToInts(lengthValues));
    } else {
      return;
    }
    AnnotationMirror newRecAnno = rolv.createAnnotation(atypeFactory);
    AnnotationMirror oldRecAnno = getArrayOrStringAnnotation(receiverNode);

    AnnotationMirror combinedRecAnno;
    // If the receiver doesn't have an @ArrayLen annotation, use the new annotation.
    // If it does have an annotation, combine the facts known about the receiver
    // with the facts known about its length using GLB.
    if (oldRecAnno == null) {
      combinedRecAnno = newRecAnno;
    } else {
      TypeMirror receiverTM = receiverJE.getType();
      combinedRecAnno =
          qualHierarchy.greatestLowerBoundShallow(oldRecAnno, receiverTM, newRecAnno, receiverTM);
    }
    store.insertValue(receiverJE, combinedRecAnno);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitStringConcatenate(
      StringConcatenateNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> result = super.visitStringConcatenate(n, p);
    return stringConcatenation(n.getLeftOperand(), n.getRightOperand(), p, result);
  }

  /**
   * Calculates possible lengths of a result of string concatenation of strings with known lengths.
   */
  private List<Integer> calculateLengthAddition(
      List<Integer> leftLengths, List<Integer> rightLengths) {
    List<Integer> result = new ArrayList<>(leftLengths.size() * rightLengths.size());

    for (int left : leftLengths) {
      for (int right : rightLengths) {
        long resultLength = (long) left + right;
        // Lengths not fitting into int are not allowed
        if (resultLength <= Integer.MAX_VALUE) {
          result.add((int) resultLength);
        }
      }
    }

    return result;
  }

  /**
   * Calculates a range of possible lengths of a result of string concatenation of strings with
   * known ranges of lengths.
   */
  private Range calculateLengthRangeAddition(Range leftLengths, Range rightLengths) {
    return leftLengths.plus(rightLengths).intersect(Range.INT_EVERYTHING);
  }

  /**
   * Checks whether or not the passed node is nullable. This superficial check assumes that every
   * node is nullable unless it is a primitive, String literal, or compile-time constant.
   *
   * @return false if the node's run-time can't be null; true if the node's run-time value may be
   *     null, or if this method is not precise enough
   */
  private boolean isNullable(Node node) {
    if (node instanceof StringConversionNode) {
      if (((StringConversionNode) node).getOperand().getType().getKind().isPrimitive()) {
        return false;
      }
    } else if (node instanceof StringLiteralNode) {
      return false;
    } else if (node instanceof StringConcatenateNode) {
      return false;
    }

    Element element = TreeUtils.elementFromTree((ExpressionTree) node.getTree());
    return !ElementUtils.isCompileTimeConstant(element);
  }

  /** Creates an annotation for a result of string concatenation. */
  private AnnotationMirror createAnnotationForStringConcatenation(
      Node leftOperand, Node rightOperand, TransferInput<CFValue, CFStore> p) {

    // Try using sets of string values
    List<String> leftValues = getStringValues(leftOperand, p);
    List<String> rightValues = getStringValues(rightOperand, p);

    if (leftValues != null && rightValues != null) {
      // Both operands have known string values, compute set of results
      if (!nonNullStringsConcatenation) {
        if (isNullable(leftOperand)) {
          leftValues = CollectionsPlume.append(leftValues, "null");
        }
        if (isNullable(rightOperand)) {
          rightValues = CollectionsPlume.append(rightValues, "null");
        }
      } else {
        if (leftOperand instanceof StringConversionNode) {
          if (((StringConversionNode) leftOperand).getOperand().getType().getKind()
              == TypeKind.NULL) {
            leftValues = CollectionsPlume.append(leftValues, "null");
          }
        }
        if (rightOperand instanceof StringConversionNode) {
          if (((StringConversionNode) rightOperand).getOperand().getType().getKind()
              == TypeKind.NULL) {
            rightValues = CollectionsPlume.append(rightValues, "null");
          }
        }
      }

      List<String> concatValues = new ArrayList<>(leftValues.size() * rightValues.size());
      for (String left : leftValues) {
        for (String right : rightValues) {
          concatValues.add(left + right);
        }
      }
      return atypeFactory.createStringAnnotation(concatValues);
    }

    // Try using sets of lengths
    List<Integer> leftLengths =
        leftValues != null
            ? ValueCheckerUtils.getLengthsForStringValues(leftValues)
            : getStringLengths(leftOperand, p);
    List<Integer> rightLengths =
        rightValues != null
            ? ValueCheckerUtils.getLengthsForStringValues(rightValues)
            : getStringLengths(rightOperand, p);

    if (leftLengths != null && rightLengths != null) {
      // Both operands have known lengths, compute set of result lengths
      if (!nonNullStringsConcatenation) {
        if (isNullable(leftOperand)) {
          leftLengths = new ArrayList<>(leftLengths);
          leftLengths.add(4); // "null"
        }
        if (isNullable(rightOperand)) {
          rightLengths = new ArrayList<>(rightLengths);
          rightLengths.add(4); // "null"
        }
      }
      List<Integer> concatLengths = calculateLengthAddition(leftLengths, rightLengths);
      return atypeFactory.createArrayLenAnnotation(concatLengths);
    }

    // Try using ranges of lengths
    Range leftLengthRange =
        leftLengths != null
            ? ValueCheckerUtils.getRangeFromValues(leftLengths)
            : getStringLengthRange(leftOperand, p);
    Range rightLengthRange =
        rightLengths != null
            ? ValueCheckerUtils.getRangeFromValues(rightLengths)
            : getStringLengthRange(rightOperand, p);

    if (leftLengthRange != null && rightLengthRange != null) {
      // Both operands have a length from a known range, compute a range of result lengths
      if (!nonNullStringsConcatenation) {
        if (isNullable(leftOperand)) {
          leftLengthRange = leftLengthRange.union(Range.create(4, 4)); // "null"
        }
        if (isNullable(rightOperand)) {
          rightLengthRange = rightLengthRange.union(Range.create(4, 4)); // "null"
        }
      }
      Range concatLengthRange = calculateLengthRangeAddition(leftLengthRange, rightLengthRange);
      return atypeFactory.createArrayLenRangeAnnotation(concatLengthRange);
    }

    return atypeFactory.UNKNOWNVAL;
  }

  public TransferResult<CFValue, CFStore> stringConcatenation(
      Node leftOperand,
      Node rightOperand,
      TransferInput<CFValue, CFStore> p,
      TransferResult<CFValue, CFStore> result) {
    AnnotationMirror resultAnno =
        createAnnotationForStringConcatenation(leftOperand, rightOperand, p);
    return recreateTransferResult(resultAnno, result);
  }

  /** Binary operations that are analyzed by the value checker. */
  enum NumericalBinaryOps {
    ADDITION,
    SUBTRACTION,
    DIVISION,
    REMAINDER,
    MULTIPLICATION,
    SHIFT_LEFT,
    SIGNED_SHIFT_RIGHT,
    UNSIGNED_SHIFT_RIGHT,
    BITWISE_AND,
    BITWISE_OR,
    BITWISE_XOR;
  }

  /**
   * Get the refined annotation after a numerical binary operation.
   *
   * @param leftNode the node that represents the left operand
   * @param rightNode the node that represents the right operand
   * @param op the operator type
   * @param p the transfer input
   * @return the result annotation mirror
   */
  private AnnotationMirror calculateNumericalBinaryOp(
      Node leftNode, Node rightNode, NumericalBinaryOps op, TransferInput<CFValue, CFStore> p) {
    if (!isIntRangeOrIntegralUnknownVal(leftNode, p)
        && !isIntRangeOrIntegralUnknownVal(rightNode, p)) {
      List<Number> resultValues = calculateValuesBinaryOp(leftNode, rightNode, op, p);
      return atypeFactory.createNumberAnnotationMirror(resultValues);
    } else {
      Range resultRange = calculateRangeBinaryOp(leftNode, rightNode, op, p);
      return atypeFactory.createIntRangeAnnotation(resultRange);
    }
  }

  /** Calculate the result range after a binary operation between two numerical type nodes. */
  private Range calculateRangeBinaryOp(
      Node leftNode, Node rightNode, NumericalBinaryOps op, TransferInput<CFValue, CFStore> p) {
    if (TypesUtils.isIntegralPrimitive(leftNode.getType())
        && TypesUtils.isIntegralPrimitive(rightNode.getType())) {
      Range leftRange = getIntRange(leftNode, p);
      Range rightRange = getIntRange(rightNode, p);
      Range resultRange;
      switch (op) {
        case ADDITION:
          resultRange = leftRange.plus(rightRange);
          break;
        case SUBTRACTION:
          resultRange = leftRange.minus(rightRange);
          break;
        case MULTIPLICATION:
          resultRange = leftRange.times(rightRange);
          break;
        case DIVISION:
          resultRange = leftRange.divide(rightRange);
          break;
        case REMAINDER:
          resultRange = leftRange.remainder(rightRange);
          break;
        case SHIFT_LEFT:
          resultRange = leftRange.shiftLeft(rightRange);
          break;
        case SIGNED_SHIFT_RIGHT:
          resultRange = leftRange.signedShiftRight(rightRange);
          break;
        case UNSIGNED_SHIFT_RIGHT:
          resultRange = leftRange.unsignedShiftRight(rightRange);
          break;
        case BITWISE_AND:
          resultRange = leftRange.bitwiseAnd(rightRange);
          break;
        case BITWISE_OR:
          resultRange = leftRange.bitwiseOr(rightRange);
          break;
        case BITWISE_XOR:
          resultRange = leftRange.bitwiseXor(rightRange);
          break;
        default:
          throw new TypeSystemError("ValueTransfer: unsupported operation: " + op);
      }
      // Any integral type with less than 32 bits would be promoted to 32-bit int type during
      // operations.
      return leftNode.getType().getKind() == TypeKind.LONG
              || rightNode.getType().getKind() == TypeKind.LONG
          ? resultRange
          : resultRange.intRange();
    } else {
      return Range.EVERYTHING;
    }
  }

  /** Calculate the possible values after a binary operation between two numerical type nodes. */
  private @Nullable List<Number> calculateValuesBinaryOp(
      Node leftNode, Node rightNode, NumericalBinaryOps op, TransferInput<CFValue, CFStore> p) {
    List<? extends Number> lefts = getNumericalValues(leftNode, p);
    List<? extends Number> rights = getNumericalValues(rightNode, p);
    if (lefts == null || rights == null) {
      return null;
    }
    List<Number> resultValues = new ArrayList<>(lefts.size() * rights.size());
    for (Number left : lefts) {
      NumberMath<?> nmLeft = NumberMath.getNumberMath(left);
      for (Number right : rights) {
        switch (op) {
          case ADDITION:
            resultValues.add(nmLeft.plus(right));
            break;
          case DIVISION:
            Number result = nmLeft.divide(right);
            if (result != null) {
              resultValues.add(result);
            }
            break;
          case MULTIPLICATION:
            resultValues.add(nmLeft.times(right));
            break;
          case REMAINDER:
            Number resultR = nmLeft.remainder(right);
            if (resultR != null) {
              resultValues.add(resultR);
            }
            break;
          case SUBTRACTION:
            resultValues.add(nmLeft.minus(right));
            break;
          case SHIFT_LEFT:
            resultValues.add(nmLeft.shiftLeft(right));
            break;
          case SIGNED_SHIFT_RIGHT:
            resultValues.add(nmLeft.signedShiftRight(right));
            break;
          case UNSIGNED_SHIFT_RIGHT:
            resultValues.add(nmLeft.unsignedShiftRight(right));
            break;
          case BITWISE_AND:
            resultValues.add(nmLeft.bitwiseAnd(right));
            break;
          case BITWISE_OR:
            resultValues.add(nmLeft.bitwiseOr(right));
            break;
          case BITWISE_XOR:
            resultValues.add(nmLeft.bitwiseXor(right));
            break;
          default:
            throw new TypeSystemError("ValueTransfer: unsupported operation: " + op);
        }
      }
    }
    return resultValues;
  }

  @Override
  public TransferResult<CFValue, CFStore> visitNumericalAddition(
      NumericalAdditionNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitNumericalAddition(n, p);
    AnnotationMirror resultAnno =
        calculateNumericalBinaryOp(
            n.getLeftOperand(), n.getRightOperand(), NumericalBinaryOps.ADDITION, p);
    return recreateTransferResult(resultAnno, transferResult);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitNumericalSubtraction(
      NumericalSubtractionNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitNumericalSubtraction(n, p);
    AnnotationMirror resultAnno =
        calculateNumericalBinaryOp(
            n.getLeftOperand(), n.getRightOperand(), NumericalBinaryOps.SUBTRACTION, p);
    return recreateTransferResult(resultAnno, transferResult);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitNumericalMultiplication(
      NumericalMultiplicationNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitNumericalMultiplication(n, p);
    AnnotationMirror resultAnno =
        calculateNumericalBinaryOp(
            n.getLeftOperand(), n.getRightOperand(), NumericalBinaryOps.MULTIPLICATION, p);
    return recreateTransferResult(resultAnno, transferResult);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitIntegerDivision(
      IntegerDivisionNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitIntegerDivision(n, p);
    AnnotationMirror resultAnno =
        calculateNumericalBinaryOp(
            n.getLeftOperand(), n.getRightOperand(), NumericalBinaryOps.DIVISION, p);
    return recreateTransferResult(resultAnno, transferResult);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitFloatingDivision(
      FloatingDivisionNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitFloatingDivision(n, p);
    AnnotationMirror resultAnno =
        calculateNumericalBinaryOp(
            n.getLeftOperand(), n.getRightOperand(), NumericalBinaryOps.DIVISION, p);
    return recreateTransferResult(resultAnno, transferResult);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitIntegerRemainder(
      IntegerRemainderNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitIntegerRemainder(n, p);
    AnnotationMirror resultAnno =
        calculateNumericalBinaryOp(
            n.getLeftOperand(), n.getRightOperand(), NumericalBinaryOps.REMAINDER, p);
    return recreateTransferResult(resultAnno, transferResult);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitFloatingRemainder(
      FloatingRemainderNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitFloatingRemainder(n, p);
    AnnotationMirror resultAnno =
        calculateNumericalBinaryOp(
            n.getLeftOperand(), n.getRightOperand(), NumericalBinaryOps.REMAINDER, p);
    return recreateTransferResult(resultAnno, transferResult);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitLeftShift(
      LeftShiftNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitLeftShift(n, p);
    AnnotationMirror resultAnno =
        calculateNumericalBinaryOp(
            n.getLeftOperand(), n.getRightOperand(), NumericalBinaryOps.SHIFT_LEFT, p);
    return recreateTransferResult(resultAnno, transferResult);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitSignedRightShift(
      SignedRightShiftNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitSignedRightShift(n, p);
    AnnotationMirror resultAnno =
        calculateNumericalBinaryOp(
            n.getLeftOperand(), n.getRightOperand(), NumericalBinaryOps.SIGNED_SHIFT_RIGHT, p);
    return recreateTransferResult(resultAnno, transferResult);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitUnsignedRightShift(
      UnsignedRightShiftNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitUnsignedRightShift(n, p);
    AnnotationMirror resultAnno =
        calculateNumericalBinaryOp(
            n.getLeftOperand(), n.getRightOperand(), NumericalBinaryOps.UNSIGNED_SHIFT_RIGHT, p);
    return recreateTransferResult(resultAnno, transferResult);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitBitwiseAnd(
      BitwiseAndNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitBitwiseAnd(n, p);
    AnnotationMirror resultAnno =
        calculateNumericalBinaryOp(
            n.getLeftOperand(), n.getRightOperand(), NumericalBinaryOps.BITWISE_AND, p);
    return recreateTransferResult(resultAnno, transferResult);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitBitwiseOr(
      BitwiseOrNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitBitwiseOr(n, p);
    AnnotationMirror resultAnno =
        calculateNumericalBinaryOp(
            n.getLeftOperand(), n.getRightOperand(), NumericalBinaryOps.BITWISE_OR, p);
    return recreateTransferResult(resultAnno, transferResult);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitBitwiseXor(
      BitwiseXorNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitBitwiseXor(n, p);
    AnnotationMirror resultAnno =
        calculateNumericalBinaryOp(
            n.getLeftOperand(), n.getRightOperand(), NumericalBinaryOps.BITWISE_XOR, p);
    return recreateTransferResult(resultAnno, transferResult);
  }

  /** Unary operations that are analyzed by the value checker. */
  enum NumericalUnaryOps {
    PLUS,
    MINUS,
    BITWISE_COMPLEMENT;
  }

  /**
   * Get the refined annotation after a numerical unary operation.
   *
   * @param operand the node that represents the operand
   * @param op the operator type
   * @param p the transfer input
   * @return the result annotation mirror
   */
  private AnnotationMirror calculateNumericalUnaryOp(
      Node operand, NumericalUnaryOps op, TransferInput<CFValue, CFStore> p) {
    if (!isIntRange(operand, p)) {
      List<Number> resultValues = calculateValuesUnaryOp(operand, op, p);
      return atypeFactory.createNumberAnnotationMirror(resultValues);
    } else {
      Range resultRange = calculateRangeUnaryOp(operand, op, p);
      return atypeFactory.createIntRangeAnnotation(resultRange);
    }
  }

  /**
   * Calculate the result range after a unary operation of a numerical type node.
   *
   * @param operand the node that represents the operand
   * @param op the operator type
   * @param p the transfer input
   * @return the result annotation mirror
   */
  private Range calculateRangeUnaryOp(
      Node operand, NumericalUnaryOps op, TransferInput<CFValue, CFStore> p) {
    if (TypesUtils.isIntegralPrimitive(operand.getType())) {
      Range range = getIntRange(operand, p);
      Range resultRange;
      switch (op) {
        case PLUS:
          resultRange = range.unaryPlus();
          break;
        case MINUS:
          resultRange = range.unaryMinus();
          break;
        case BITWISE_COMPLEMENT:
          resultRange = range.bitwiseComplement();
          break;
        default:
          throw new TypeSystemError("ValueTransfer: unsupported operation: " + op);
      }
      // Any integral type with less than 32 bits would be promoted to 32-bit int type during
      // operations.
      return operand.getType().getKind() == TypeKind.LONG ? resultRange : resultRange.intRange();
    } else {
      return Range.EVERYTHING;
    }
  }

  /** Calculate the possible values after a unary operation of a numerical type node. */
  private @Nullable List<Number> calculateValuesUnaryOp(
      Node operand, NumericalUnaryOps op, TransferInput<CFValue, CFStore> p) {
    List<? extends Number> lefts = getNumericalValues(operand, p);
    if (lefts == null) {
      return null;
    }
    List<Number> resultValues = new ArrayList<>(lefts.size());
    for (Number left : lefts) {
      NumberMath<?> nmLeft = NumberMath.getNumberMath(left);
      switch (op) {
        case PLUS:
          resultValues.add(nmLeft.unaryPlus());
          break;
        case MINUS:
          resultValues.add(nmLeft.unaryMinus());
          break;
        case BITWISE_COMPLEMENT:
          resultValues.add(nmLeft.bitwiseComplement());
          break;
        default:
          throw new TypeSystemError("ValueTransfer: unsupported operation: " + op);
      }
    }
    return resultValues;
  }

  @Override
  public TransferResult<CFValue, CFStore> visitNumericalMinus(
      NumericalMinusNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitNumericalMinus(n, p);
    AnnotationMirror resultAnno =
        calculateNumericalUnaryOp(n.getOperand(), NumericalUnaryOps.MINUS, p);
    return recreateTransferResult(resultAnno, transferResult);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitNumericalPlus(
      NumericalPlusNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitNumericalPlus(n, p);
    AnnotationMirror resultAnno =
        calculateNumericalUnaryOp(n.getOperand(), NumericalUnaryOps.PLUS, p);
    return recreateTransferResult(resultAnno, transferResult);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitBitwiseComplement(
      BitwiseComplementNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitBitwiseComplement(n, p);
    AnnotationMirror resultAnno =
        calculateNumericalUnaryOp(n.getOperand(), NumericalUnaryOps.BITWISE_COMPLEMENT, p);
    return recreateTransferResult(resultAnno, transferResult);
  }

  enum ComparisonOperators {
    EQUAL,
    NOT_EQUAL,
    GREATER_THAN,
    GREATER_THAN_EQ,
    LESS_THAN,
    LESS_THAN_EQ;
  }

  private @Nullable List<Boolean> calculateBinaryComparison(
      Node leftNode,
      CFValue leftValue,
      Node rightNode,
      CFValue rightValue,
      ComparisonOperators op,
      CFStore thenStore,
      CFStore elseStore) {
    AnnotationMirror leftAnno = getValueAnnotation(leftValue);
    AnnotationMirror rightAnno = getValueAnnotation(rightValue);

    if (atypeFactory.isIntRange(leftAnno)
        || atypeFactory.isIntRange(rightAnno)
        || isIntegralUnknownVal(rightNode, rightAnno)
        || isIntegralUnknownVal(leftNode, leftAnno)) {
      // If either is @UnknownVal, then refineIntRanges will treat it as the max range and
      // thus refine it if possible.  Also, if either is an @IntVal, then it will be converted
      // to a range.  This is less precise in some cases, but avoids the complexity of
      // comparing a list of values to a range. (This could be implemented in the future.)
      return refineIntRanges(leftNode, leftAnno, rightNode, rightAnno, op, thenStore, elseStore);
    }

    List<? extends Number> lefts = getNumericalValues(leftNode, leftAnno);
    List<? extends Number> rights = getNumericalValues(rightNode, rightAnno);

    if (lefts == null || rights == null) {
      // Appropriately handle bottom when something is compared to bottom.
      if (AnnotationUtils.areSame(leftAnno, atypeFactory.BOTTOMVAL)
          || AnnotationUtils.areSame(rightAnno, atypeFactory.BOTTOMVAL)) {
        return Collections.emptyList();
      }
      return null;
    }

    // This is a list of all the values that the expression can evaluate to.
    int numResultValues = lefts.size() * rights.size();
    List<Boolean> resultValues = new ArrayList<>(numResultValues);

    // These lists are used to refine the values in the store based on the results of the
    // comparison.
    List<Number> thenLeftVals = new ArrayList<>(numResultValues);
    List<Number> elseLeftVals = new ArrayList<>(numResultValues);
    List<Number> thenRightVals = new ArrayList<>(numResultValues);
    List<Number> elseRightVals = new ArrayList<>(numResultValues);

    for (Number left : lefts) {
      NumberMath<?> nmLeft = NumberMath.getNumberMath(left);
      for (Number right : rights) {
        Boolean result;
        switch (op) {
          case EQUAL:
            result = nmLeft.equalTo(right);
            break;
          case GREATER_THAN:
            result = nmLeft.greaterThan(right);
            break;
          case GREATER_THAN_EQ:
            result = nmLeft.greaterThanEq(right);
            break;
          case LESS_THAN:
            result = nmLeft.lessThan(right);
            break;
          case LESS_THAN_EQ:
            result = nmLeft.lessThanEq(right);
            break;
          case NOT_EQUAL:
            result = nmLeft.notEqualTo(right);
            break;
          default:
            throw new TypeSystemError("ValueTransfer: unsupported operation: " + op);
        }
        resultValues.add(result);
        if (result) {
          thenLeftVals.add(left);
          thenRightVals.add(right);
        } else {
          elseLeftVals.add(left);
          elseRightVals.add(right);
        }
      }
    }

    createAnnotationFromResultsAndAddToStore(thenStore, thenLeftVals, leftNode);
    createAnnotationFromResultsAndAddToStore(elseStore, elseLeftVals, leftNode);
    createAnnotationFromResultsAndAddToStore(thenStore, thenRightVals, rightNode);
    createAnnotationFromResultsAndAddToStore(elseStore, elseRightVals, rightNode);

    return resultValues;
  }

  /**
   * Calculates the result of a binary comparison on a pair of intRange annotations, and refines
   * annotations appropriately.
   */
  private @Nullable List<Boolean> refineIntRanges(
      Node leftNode,
      AnnotationMirror leftAnno,
      Node rightNode,
      AnnotationMirror rightAnno,
      ComparisonOperators op,
      CFStore thenStore,
      CFStore elseStore) {

    Range leftRange = getIntRangeFromAnnotation(leftNode, leftAnno);
    Range rightRange = getIntRangeFromAnnotation(rightNode, rightAnno);

    final Range thenRightRange;
    final Range thenLeftRange;
    final Range elseRightRange;
    final Range elseLeftRange;

    switch (op) {
      case EQUAL:
        thenRightRange = rightRange.refineEqualTo(leftRange);
        thenLeftRange = thenRightRange; // Only needs to be computed once.
        elseRightRange = rightRange.refineNotEqualTo(leftRange);
        elseLeftRange = leftRange.refineNotEqualTo(rightRange);
        break;
      case GREATER_THAN:
        thenLeftRange = leftRange.refineGreaterThan(rightRange);
        thenRightRange = rightRange.refineLessThan(leftRange);
        elseRightRange = rightRange.refineGreaterThanEq(leftRange);
        elseLeftRange = leftRange.refineLessThanEq(rightRange);
        break;
      case GREATER_THAN_EQ:
        thenRightRange = rightRange.refineLessThanEq(leftRange);
        thenLeftRange = leftRange.refineGreaterThanEq(rightRange);
        elseLeftRange = leftRange.refineLessThan(rightRange);
        elseRightRange = rightRange.refineGreaterThan(leftRange);
        break;
      case LESS_THAN:
        thenLeftRange = leftRange.refineLessThan(rightRange);
        thenRightRange = rightRange.refineGreaterThan(leftRange);
        elseRightRange = rightRange.refineLessThanEq(leftRange);
        elseLeftRange = leftRange.refineGreaterThanEq(rightRange);
        break;
      case LESS_THAN_EQ:
        thenRightRange = rightRange.refineGreaterThanEq(leftRange);
        thenLeftRange = leftRange.refineLessThanEq(rightRange);
        elseLeftRange = leftRange.refineGreaterThan(rightRange);
        elseRightRange = rightRange.refineLessThan(leftRange);
        break;
      case NOT_EQUAL:
        thenRightRange = rightRange.refineNotEqualTo(leftRange);
        thenLeftRange = leftRange.refineNotEqualTo(rightRange);
        elseRightRange = rightRange.refineEqualTo(leftRange);
        elseLeftRange = elseRightRange; // Equality only needs to be computed once.
        break;
      default:
        throw new TypeSystemError("ValueTransfer: unsupported operation: " + op);
    }

    createAnnotationFromRangeAndAddToStore(thenStore, thenRightRange, rightNode);
    createAnnotationFromRangeAndAddToStore(thenStore, thenLeftRange, leftNode);
    createAnnotationFromRangeAndAddToStore(elseStore, elseRightRange, rightNode);
    createAnnotationFromRangeAndAddToStore(elseStore, elseLeftRange, leftNode);

    // TODO: Refine the type of the comparison.
    return null;
  }

  /**
   * Takes a list of result values (i.e. the values possible after the comparison) and creates the
   * appropriate annotation from them, then combines that annotation with the existing annotation on
   * the node. The resulting annotation is inserted into the store.
   *
   * @param store the store
   * @param results the result values
   * @param node the node whose existing annotation to refine
   */
  private void createAnnotationFromResultsAndAddToStore(CFStore store, List<?> results, Node node) {
    AnnotationMirror anno = atypeFactory.createResultingAnnotation(node.getType(), results);
    addAnnotationToStore(store, anno, node);
  }

  /**
   * Takes a range and creates the appropriate annotation from it, then combines that annotation
   * with the existing annotation on the node. The resulting annotation is inserted into the store.
   *
   * @param store the store
   * @param range the range to create an annotation for
   * @param node the node whose existing annotation to refine
   */
  private void createAnnotationFromRangeAndAddToStore(CFStore store, Range range, Node node) {
    AnnotationMirror anno = atypeFactory.createIntRangeAnnotation(range);
    addAnnotationToStore(store, anno, node);
  }

  private void addAnnotationToStore(CFStore store, AnnotationMirror anno, Node node) {
    // If node is assignment, iterate over lhs and rhs; otherwise, iterator contains just node.
    for (Node internal : splitAssignments(node)) {
      JavaExpression je = JavaExpression.fromNode(internal);
      CFValue currentValueFromStore;
      if (CFAbstractStore.canInsertJavaExpression(je)) {
        currentValueFromStore = store.getValue(je);
      } else {
        // Don't just `continue;` which would skip the calls to refine{Array,String}...
        currentValueFromStore = null;
      }
      AnnotationMirror currentAnno =
          (currentValueFromStore == null
              ? atypeFactory.UNKNOWNVAL
              : getValueAnnotation(currentValueFromStore));
      // Combine the new annotations based on the results of the comparison with the existing
      // type.
      AnnotationMirror newAnno =
          qualHierarchy.greatestLowerBoundShallow(anno, je.getType(), currentAnno, je.getType());
      store.insertValue(je, newAnno);

      if (node instanceof FieldAccessNode) {
        refineArrayAtLengthAccess((FieldAccessNode) internal, store);
      } else if (node instanceof MethodInvocationNode) {
        MethodInvocationNode miNode = (MethodInvocationNode) node;
        refineAtLengthInvocation(miNode, store);
      }
    }
  }

  @Override
  public TransferResult<CFValue, CFStore> visitLessThan(
      LessThanNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitLessThan(n, p);
    CFStore thenStore = transferResult.getThenStore();
    CFStore elseStore = transferResult.getElseStore();
    List<Boolean> resultValues =
        calculateBinaryComparison(
            n.getLeftOperand(),
            p.getValueOfSubNode(n.getLeftOperand()),
            n.getRightOperand(),
            p.getValueOfSubNode(n.getRightOperand()),
            ComparisonOperators.LESS_THAN,
            thenStore,
            elseStore);
    TypeMirror underlyingType = transferResult.getResultValue().getUnderlyingType();
    return createNewResultBoolean(thenStore, elseStore, resultValues, underlyingType);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitLessThanOrEqual(
      LessThanOrEqualNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitLessThanOrEqual(n, p);
    CFStore thenStore = transferResult.getThenStore();
    CFStore elseStore = transferResult.getElseStore();
    List<Boolean> resultValues =
        calculateBinaryComparison(
            n.getLeftOperand(),
            p.getValueOfSubNode(n.getLeftOperand()),
            n.getRightOperand(),
            p.getValueOfSubNode(n.getRightOperand()),
            ComparisonOperators.LESS_THAN_EQ,
            thenStore,
            elseStore);
    TypeMirror underlyingType = transferResult.getResultValue().getUnderlyingType();
    return createNewResultBoolean(thenStore, elseStore, resultValues, underlyingType);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitGreaterThan(
      GreaterThanNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitGreaterThan(n, p);
    CFStore thenStore = transferResult.getThenStore();
    CFStore elseStore = transferResult.getElseStore();
    List<Boolean> resultValues =
        calculateBinaryComparison(
            n.getLeftOperand(),
            p.getValueOfSubNode(n.getLeftOperand()),
            n.getRightOperand(),
            p.getValueOfSubNode(n.getRightOperand()),
            ComparisonOperators.GREATER_THAN,
            thenStore,
            elseStore);
    TypeMirror underlyingType = transferResult.getResultValue().getUnderlyingType();
    return createNewResultBoolean(thenStore, elseStore, resultValues, underlyingType);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitGreaterThanOrEqual(
      GreaterThanOrEqualNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitGreaterThanOrEqual(n, p);
    CFStore thenStore = transferResult.getThenStore();
    CFStore elseStore = transferResult.getElseStore();
    List<Boolean> resultValues =
        calculateBinaryComparison(
            n.getLeftOperand(),
            p.getValueOfSubNode(n.getLeftOperand()),
            n.getRightOperand(),
            p.getValueOfSubNode(n.getRightOperand()),
            ComparisonOperators.GREATER_THAN_EQ,
            thenStore,
            elseStore);
    TypeMirror underlyingType = transferResult.getResultValue().getUnderlyingType();
    return createNewResultBoolean(thenStore, elseStore, resultValues, underlyingType);
  }

  @Override
  protected TransferResult<CFValue, CFStore> strengthenAnnotationOfEqualTo(
      TransferResult<CFValue, CFStore> transferResult,
      Node firstNode,
      Node secondNode,
      CFValue firstValue,
      CFValue secondValue,
      boolean notEqualTo) {
    if (firstValue == null) {
      return transferResult;
    }
    if (TypesUtils.isNumeric(firstNode.getType()) || TypesUtils.isNumeric(secondNode.getType())) {
      CFStore thenStore = transferResult.getThenStore();
      CFStore elseStore = transferResult.getElseStore();
      // At least one must be a primitive otherwise reference equality is used.
      List<Boolean> resultValues =
          calculateBinaryComparison(
              firstNode,
              firstValue,
              secondNode,
              secondValue,
              notEqualTo ? ComparisonOperators.NOT_EQUAL : ComparisonOperators.EQUAL,
              thenStore,
              elseStore);
      if (transferResult.getResultValue() == null) {
        // Happens for case labels
        return transferResult;
      }
      TypeMirror underlyingType = transferResult.getResultValue().getUnderlyingType();
      return createNewResultBoolean(thenStore, elseStore, resultValues, underlyingType);
    }
    return super.strengthenAnnotationOfEqualTo(
        transferResult, firstNode, secondNode, firstValue, secondValue, notEqualTo);
  }

  @Override
  protected void processConditionalPostconditions(
      MethodInvocationNode n,
      ExecutableElement methodElement,
      ExpressionTree tree,
      CFStore thenStore,
      CFStore elseStore) {
    // For String.startsWith(String) and String.endsWith(String), refine the minimum length
    // of the receiver to the minimum length of the argument.
    ValueMethodIdentifier methodIdentifier = atypeFactory.getMethodIdentifier();
    if (methodIdentifier.isStartsWithMethod(methodElement)
        || methodIdentifier.isEndsWithMethod(methodElement)) {

      Node argumentNode = n.getArgument(0);
      AnnotationMirror argumentAnno = getArrayOrStringAnnotation(argumentNode);
      int minLength = atypeFactory.getMinLenValue(argumentAnno);
      // Update the annotation of the receiver
      if (minLength != 0) {
        JavaExpression receiver = JavaExpression.fromNode(n.getTarget().getReceiver());

        AnnotationMirror minLenAnno =
            atypeFactory.createArrayLenRangeAnnotation(minLength, Integer.MAX_VALUE);
        thenStore.insertValuePermitNondeterministic(receiver, minLenAnno);
      }
    }

    super.processConditionalPostconditions(n, methodElement, tree, thenStore, elseStore);
  }

  enum ConditionalOperators {
    NOT,
    OR,
    AND;
  }

  private static final List<Boolean> ALL_BOOLEANS =
      Arrays.asList(new Boolean[] {Boolean.TRUE, Boolean.FALSE});

  private List<Boolean> calculateConditionalOperator(
      Node leftNode, Node rightNode, ConditionalOperators op, TransferInput<CFValue, CFStore> p) {
    List<Boolean> lefts = getBooleanValues(leftNode, p);
    if (lefts == null) {
      lefts = ALL_BOOLEANS;
    }
    List<Boolean> rights = null;
    if (rightNode != null) {
      rights = getBooleanValues(rightNode, p);
      if (rights == null) {
        rights = ALL_BOOLEANS;
      }
    }
    // This list can contain duplicates.  It is deduplicated later by createBooleanAnnotation.
    List<Boolean> resultValues = new ArrayList<>(2);
    switch (op) {
      case NOT:
        return CollectionsPlume.mapList((Boolean left) -> !left, lefts);
      case OR:
        for (Boolean left : lefts) {
          for (Boolean right : rights) {
            resultValues.add(left || right);
          }
        }
        return resultValues;
      case AND:
        for (Boolean left : lefts) {
          for (Boolean right : rights) {
            resultValues.add(left && right);
          }
        }
        return resultValues;
    }
    throw new TypeSystemError("ValueTransfer: unsupported operation: " + op);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitEqualTo(
      EqualToNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> res = super.visitEqualTo(n, p);

    Node leftN = n.getLeftOperand();
    Node rightN = n.getRightOperand();
    CFValue leftV = p.getValueOfSubNode(leftN);
    CFValue rightV = p.getValueOfSubNode(rightN);

    // if annotations differ, use the one that is more precise for both
    // sides (and add it to the store if possible)
    res = strengthenAnnotationOfEqualTo(res, leftN, rightN, leftV, rightV, false);
    res = strengthenAnnotationOfEqualTo(res, rightN, leftN, rightV, leftV, false);

    Boolean leftBoolean = getBooleanValue(leftV);
    if (leftBoolean != null) {
      CFValue notLeftV = createBooleanCFValue(!leftBoolean);
      res = strengthenAnnotationOfEqualTo(res, leftN, rightN, notLeftV, rightV, true);
      res = strengthenAnnotationOfEqualTo(res, rightN, leftN, rightV, notLeftV, true);
    }
    Boolean rightBoolean = getBooleanValue(rightV);
    if (rightBoolean != null) {
      CFValue notRightV = createBooleanCFValue(!rightBoolean);
      res = strengthenAnnotationOfEqualTo(res, leftN, rightN, leftV, notRightV, true);
      res = strengthenAnnotationOfEqualTo(res, rightN, leftN, notRightV, leftV, true);
    }

    return res;
  }

  @Override
  public TransferResult<CFValue, CFStore> visitNotEqual(
      NotEqualNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> res = super.visitNotEqual(n, p);

    Node leftN = n.getLeftOperand();
    Node rightN = n.getRightOperand();
    CFValue leftV = p.getValueOfSubNode(leftN);
    CFValue rightV = p.getValueOfSubNode(rightN);

    // if annotations differ, use the one that is more precise for both
    // sides (and add it to the store if possible)
    res = strengthenAnnotationOfEqualTo(res, leftN, rightN, leftV, rightV, true);
    res = strengthenAnnotationOfEqualTo(res, rightN, leftN, rightV, leftV, true);

    Boolean leftBoolean = getBooleanValue(leftV);
    if (leftBoolean != null) {
      CFValue notLeftV = createBooleanCFValue(!leftBoolean);
      res = strengthenAnnotationOfEqualTo(res, leftN, rightN, notLeftV, rightV, false);
      res = strengthenAnnotationOfEqualTo(res, rightN, leftN, rightV, notLeftV, false);
    }
    Boolean rightBoolean = getBooleanValue(rightV);
    if (rightBoolean != null) {
      CFValue notRightV = createBooleanCFValue(!rightBoolean);
      res = strengthenAnnotationOfEqualTo(res, leftN, rightN, leftV, notRightV, false);
      res = strengthenAnnotationOfEqualTo(res, rightN, leftN, notRightV, leftV, false);
    }

    return res;
  }

  @Override
  public TransferResult<CFValue, CFStore> visitConditionalNot(
      ConditionalNotNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitConditionalNot(n, p);
    List<Boolean> resultValues =
        calculateConditionalOperator(n.getOperand(), null, ConditionalOperators.NOT, p);
    return createNewResultBoolean(
        transferResult.getThenStore(),
        transferResult.getElseStore(),
        resultValues,
        transferResult.getResultValue().getUnderlyingType());
  }

  @Override
  public TransferResult<CFValue, CFStore> visitConditionalAnd(
      ConditionalAndNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitConditionalAnd(n, p);
    List<Boolean> resultValues =
        calculateConditionalOperator(
            n.getLeftOperand(), n.getRightOperand(), ConditionalOperators.AND, p);
    return createNewResultBoolean(
        transferResult.getThenStore(),
        transferResult.getElseStore(),
        resultValues,
        transferResult.getResultValue().getUnderlyingType());
  }

  @Override
  public TransferResult<CFValue, CFStore> visitConditionalOr(
      ConditionalOrNode n, TransferInput<CFValue, CFStore> p) {
    TransferResult<CFValue, CFStore> transferResult = super.visitConditionalOr(n, p);
    List<Boolean> resultValues =
        calculateConditionalOperator(
            n.getLeftOperand(), n.getRightOperand(), ConditionalOperators.OR, p);
    return createNewResultBoolean(
        transferResult.getThenStore(),
        transferResult.getElseStore(),
        resultValues,
        transferResult.getResultValue().getUnderlyingType());
  }
}
