package org.checkerframework.common.value;

import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.checkerframework.common.value.util.Range;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.typeannotator.TypeAnnotator;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.TypeKindUtils;
import org.checkerframework.javacutil.TypesUtils;

/**
 * Performs pre-processing on annotations written by users, replacing illegal annotations by legal
 * ones.
 */
class ValueTypeAnnotator extends TypeAnnotator {

  /** The type factory to use. Shadows the field from the superclass with a more specific type. */
  @SuppressWarnings("HidingField")
  protected final ValueAnnotatedTypeFactory typeFactory;

  /**
   * Construct a new ValueTypeAnnotator.
   *
   * @param typeFactory the type factory to use
   */
  protected ValueTypeAnnotator(ValueAnnotatedTypeFactory typeFactory) {
    super(typeFactory);
    this.typeFactory = typeFactory;
  }

  @Override
  protected Void scan(AnnotatedTypeMirror type, Void aVoid) {
    replaceWithNewAnnoInSpecialCases(type);
    return super.scan(type, aVoid);
  }

  /**
   * This method performs pre-processing on annotations written by users.
   *
   * <p>If any *Val annotation has &gt; MAX_VALUES number of values provided, replaces the
   * annotation by @IntRange for integral types, @ArrayLenRange for arrays, @ArrayLen
   * or @ArrayLenRange for strings, and @UnknownVal for all other types. Works together with {@link
   * ValueVisitor#visitAnnotation(com.sun.source.tree.AnnotationTree, Void)} which issues warnings
   * to users in these cases.
   *
   * <p>If any @IntRange or @ArrayLenRange annotation has incorrect parameters, e.g. the value
   * "from" is greater than the value "to", replaces the annotation by {@code @BottomVal}. The
   * {@link ValueVisitor#visitAnnotation(com.sun.source.tree.AnnotationTree, Void)} raises an error
   * to users if the annotation was user-written.
   *
   * <p>If any @ArrayLen annotation has a negative number, replaces the annotation by {@code
   * BottomVal}. The {@link ValueVisitor#visitAnnotation(com.sun.source.tree.AnnotationTree, Void)}
   * raises an error to users if the annotation was user-written.
   *
   * <p>If a user only writes one side of an {@code IntRange} annotation, this method also computes
   * an appropriate default based on the underlying type for the other side of the range. For
   * instance, if the user writes {@code @IntRange(from = 1) short x;} then this method will
   * translate the annotation to {@code @IntRange(from = 1, to = Short.MAX_VALUE}.
   */
  private void replaceWithNewAnnoInSpecialCases(AnnotatedTypeMirror atm) {
    AnnotationMirror anno = atm.getPrimaryAnnotationInHierarchy(typeFactory.UNKNOWNVAL);
    if (anno == null || anno.getElementValues().isEmpty()) {
      return;
    }

    if (AnnotationUtils.areSameByName(anno, ValueAnnotatedTypeFactory.INTVAL_NAME)) {
      List<Long> values = typeFactory.getIntValues(anno);
      if (values.size() > ValueAnnotatedTypeFactory.MAX_VALUES) {
        atm.replaceAnnotation(typeFactory.createIntRangeAnnotation(Range.create(values)));
      }
    } else if (AnnotationUtils.areSameByName(anno, ValueAnnotatedTypeFactory.ARRAYLEN_NAME)) {
      List<Integer> values = typeFactory.getArrayLength(anno);
      if (values.isEmpty()) {
        atm.replaceAnnotation(typeFactory.BOTTOMVAL);
      } else if (Collections.min(values) < 0) {
        atm.replaceAnnotation(typeFactory.BOTTOMVAL);
      } else if (values.size() > ValueAnnotatedTypeFactory.MAX_VALUES) {
        atm.replaceAnnotation(typeFactory.createArrayLenRangeAnnotation(Range.create(values)));
      }
    } else if (AnnotationUtils.areSameByName(anno, ValueAnnotatedTypeFactory.INTRANGE_NAME)) {
      TypeMirror underlyingType = atm.getUnderlyingType();
      // If the underlying type is neither a primitive integral type nor boxed integral type,
      // return without making changes. TypesUtils.isIntegralPrimitiveOrBoxed fails if passed
      // a non-primitive type that is not a declared type, so it cannot be called directly.
      if (!TypeKindUtils.isIntegral(underlyingType.getKind())
          && (underlyingType.getKind() != TypeKind.DECLARED
              || !TypesUtils.isIntegralPrimitiveOrBoxed(underlyingType))) {
        return;
      }

      // Compute appropriate defaults for integral ranges.
      long from = typeFactory.getFromValueFromIntRange(atm);
      long to = typeFactory.getToValueFromIntRange(atm);

      if (from > to) {
        // `from > to` either indicates a user error when writing an annotation or an error
        // in the checker's implementation. `-from` should always be <= to.
        // ValueVisitor#validateType will issue an error.
        atm.replaceAnnotation(typeFactory.BOTTOMVAL);
      } else {
        // Always do a replacement of the annotation here so that the defaults calculated
        // above are correctly added to the annotation (assuming the annotation is
        // well-formed).
        atm.replaceAnnotation(typeFactory.createIntRangeAnnotation(from, to));
      }
    } else if (AnnotationUtils.areSameByName(anno, ValueAnnotatedTypeFactory.ARRAYLENRANGE_NAME)) {
      int from = typeFactory.getArrayLenRangeFromValue(anno);
      int to = typeFactory.getArrayLenRangeToValue(anno);
      if (from > to) {
        // `from > to` either indicates a user error when writing an annotation or an error
        // in the checker's implementation `-from` should always be <= to.
        // ValueVisitor#validateType will issue an error.
        atm.replaceAnnotation(typeFactory.BOTTOMVAL);
      } else if (from < 0) {
        // No array can have a length less than 0. Any time the type includes a from
        // less than zero, it must indicate imprecision in the checker.
        atm.replaceAnnotation(typeFactory.createArrayLenRangeAnnotation(0, to));
      }
    } else if (AnnotationUtils.areSameByName(anno, ValueAnnotatedTypeFactory.STRINGVAL_NAME)) {
      // The annotation is StringVal. If there are too many elements,
      // ArrayLen or ArrayLenRange is used.
      List<String> values = typeFactory.getStringValues(anno);

      if (values.size() > ValueAnnotatedTypeFactory.MAX_VALUES) {
        List<Integer> lengths = ValueCheckerUtils.getLengthsForStringValues(values);
        atm.replaceAnnotation(typeFactory.createArrayLenAnnotation(lengths));
      }

    } else if (AnnotationUtils.areSameByName(anno, ValueAnnotatedTypeFactory.MATCHES_REGEX_NAME)) {
      // If the annotation contains an invalid regex, replace it with bottom. ValueVisitor
      // will issue a warning where the annotation was written.
      List<String> regexes =
          AnnotationUtils.getElementValueArray(
              anno, typeFactory.matchesRegexValueElement, String.class);
      if (!allRegexesCompile(regexes)) {
        atm.replaceAnnotation(typeFactory.BOTTOMVAL);
      }
    } else if (AnnotationUtils.areSameByName(
        anno, ValueAnnotatedTypeFactory.DOES_NOT_MATCH_REGEX_NAME)) {
      // If the annotation contains an invalid regex, replace it with bottom. ValueVisitor
      // will issue a warning where the annotation was written.
      List<String> regexes =
          AnnotationUtils.getElementValueArray(
              anno, typeFactory.doesNotMatchRegexValueElement, String.class);
      if (!allRegexesCompile(regexes)) {
        atm.replaceAnnotation(typeFactory.BOTTOMVAL);
      }
    } else {
      // In here the annotation is @*Val where (*) is not Int, String but other types
      // (Bool, Double, or Enum).
      // Therefore we extract its values in a generic way to check its size.
      @SuppressWarnings("deprecation") // concrete annotation class is not known
      List<Object> values =
          AnnotationUtils.getElementValueArray(anno, "value", Object.class, false);
      if (values.size() > ValueAnnotatedTypeFactory.MAX_VALUES) {
        atm.replaceAnnotation(typeFactory.UNKNOWNVAL);
      }
    }
  }

  /**
   * Returns true if all the given strings are valid regexes.
   *
   * @param regexes a list of strings that might all be regexes
   * @return true if all the given strings are valid regexes
   */
  private boolean allRegexesCompile(List<String> regexes) {
    for (String regex : regexes) {
      try {
        Pattern.compile(regex);
      } catch (PatternSyntaxException e) {
        return false;
      }
    }
    return true;
  }
}
