package org.checkerframework.framework.util;

import com.sun.source.tree.Tree;
import java.lang.annotation.Annotation;
import java.util.Objects;
import javax.lang.model.element.AnnotationMirror;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.dataflow.expression.JavaExpression;
import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation;
import org.checkerframework.framework.qual.EnsuresQualifier;
import org.checkerframework.framework.qual.EnsuresQualifierIf;
import org.checkerframework.framework.qual.PostconditionAnnotation;
import org.checkerframework.framework.qual.PreconditionAnnotation;
import org.checkerframework.framework.qual.RequiresQualifier;
import org.checkerframework.framework.type.GenericAnnotatedTypeFactory;
import org.checkerframework.framework.util.dependenttypes.DependentTypesHelper;
import org.checkerframework.javacutil.BugInCF;

/**
 * A contract represents an annotation on an expression. It is a precondition, postcondition, or
 * conditional postcondition.
 *
 * @see Precondition
 * @see Postcondition
 * @see ConditionalPostcondition
 */
public abstract class Contract {

  /**
   * The expression for which the condition must hold, such as {@code "foo"} in
   * {@code @RequiresNonNull("foo")}.
   *
   * <p>An annotation like {@code @RequiresNonNull({"a", "b", "c"})} would be represented by
   * multiple Contracts.
   */
  public final String expressionString;

  /** The annotation on the type of expression, according to this contract. */
  public final AnnotationMirror annotation;

  /**
   * The annotation that expressed this contract; used for diagnostic messages, but not for the
   * location of the diagnostic message.
   */
  public final AnnotationMirror contractAnnotation;

  // This is redundant with the contract's class and is not used in this file, but the field
  // is used by clients, for its fields.
  /** The kind of contract: precondition, postcondition, or conditional postcondition. */
  public final Kind kind;

  /** Enumerates the kinds of contracts. */
  public enum Kind {
    /** A precondition. */
    PRECONDITION(
        "precondition",
        PreconditionAnnotation.class,
        RequiresQualifier.class,
        RequiresQualifier.List.class),
    /** A postcondition. */
    POSTCONDITION(
        "postcondition",
        PostconditionAnnotation.class,
        EnsuresQualifier.class,
        EnsuresQualifier.List.class),
    /** A conditional postcondition. */
    CONDITIONALPOSTCONDITION(
        "conditional postcondition",
        ConditionalPostconditionAnnotation.class,
        EnsuresQualifierIf.class,
        EnsuresQualifierIf.List.class);

    /** Used for constructing error messages. */
    public final String errorKey;

    /**
     * The meta-annotation identifying annotations of this kind: PreconditionAnnotation,
     * PostconditionAnnotation, or ConditionalPostconditionAnnotation.
     */
    public final Class<? extends Annotation> metaAnnotation;

    /**
     * The built-in framework qualifier for this contract: RequiresQualifier, EnsuresQualifier, or
     * EnsuresQualifierIf.
     */
    public final Class<? extends Annotation> frameworkContractClass;

    /**
     * The built-in framework qualifier for repeated occurrences of this contract:
     * RequiresQualifier.List, EnsuresQualifier.List, or EnsuresQualifierIf.List.
     */
    public final Class<? extends Annotation> frameworkContractListClass;

    /**
     * Create a new Kind.
     *
     * @param errorKey used for constructing error messages
     * @param metaAnnotation the meta-annotation identifying annotations of this kind
     * @param frameworkContractClass the built-in framework qualifier for this contract
     * @param frameworkContractListClass the built-in framework qualifier for repeated occurrences
     *     of this contract
     */
    Kind(
        String errorKey,
        Class<? extends Annotation> metaAnnotation,
        Class<? extends Annotation> frameworkContractClass,
        Class<? extends Annotation> frameworkContractListClass) {
      this.errorKey = errorKey;
      this.metaAnnotation = metaAnnotation;
      this.frameworkContractClass = frameworkContractClass;
      this.frameworkContractListClass = frameworkContractListClass;
    }
  }

  /**
   * Creates a new Contract. This should be called only by the constructors for {@link
   * Precondition}, {@link Postcondition}, and {@link ConditionalPostcondition}.
   *
   * @param kind precondition, postcondition, or conditional postcondition
   * @param expressionString the Java expression that should have a type qualifier
   * @param annotation the type qualifier that {@code expressionString} should have
   * @param contractAnnotation the pre- or post-condition annotation that the programmer wrote; used
   *     for diagnostic messages
   */
  private Contract(
      Kind kind,
      String expressionString,
      AnnotationMirror annotation,
      AnnotationMirror contractAnnotation) {
    this.expressionString = expressionString;
    this.annotation = annotation;
    this.contractAnnotation = contractAnnotation;
    this.kind = kind;
  }

  /**
   * Creates a new {@code Contract}.
   *
   * @param kind precondition, postcondition, or conditional postcondition
   * @param expressionString the Java expression that should have a type qualifier
   * @param annotation the type qualifier that {@code expressionString} should have
   * @param contractAnnotation the pre- or post-condition annotation that the programmer wrote; used
   *     for diagnostic messages
   * @param ensuresQualifierIf the ensuresQualifierIf field, for a conditional postcondition
   * @return a new contract
   */
  public static Contract create(
      Kind kind,
      String expressionString,
      AnnotationMirror annotation,
      AnnotationMirror contractAnnotation,
      Boolean ensuresQualifierIf) {
    if ((ensuresQualifierIf != null) != (kind == Kind.CONDITIONALPOSTCONDITION)) {
      throw new BugInCF(
          "Mismatch: Contract.create(%s, %s, %s, %s, %s)",
          kind, expressionString, annotation, contractAnnotation, ensuresQualifierIf);
    }
    switch (kind) {
      case PRECONDITION:
        return new Precondition(expressionString, annotation, contractAnnotation);
      case POSTCONDITION:
        return new Postcondition(expressionString, annotation, contractAnnotation);
      case CONDITIONALPOSTCONDITION:
        return new ConditionalPostcondition(
            expressionString, annotation, contractAnnotation, ensuresQualifierIf);
      default:
        throw new BugInCF("Unrecognized kind: " + kind);
    }
  }

  // Note that equality requires exact match of the run-time class and that it ignores the
  // `contractAnnotation` field.
  @Override
  public boolean equals(@Nullable Object o) {
    if (this == o) {
      return true;
    }
    if (o == null) {
      return false;
    }
    if (getClass() != o.getClass()) {
      return false;
    }

    Contract otherContract = (Contract) o;

    return kind == otherContract.kind
        && Objects.equals(expressionString, otherContract.expressionString)
        && Objects.equals(annotation, otherContract.annotation);
  }

  @Override
  public int hashCode() {
    return Objects.hash(kind, expressionString, annotation);
  }

  @Override
  public String toString() {
    return String.format(
        "%s{expressionString=%s, annotation=%s, contractAnnotation=%s}",
        getClass().getSimpleName(), expressionString, annotation, contractAnnotation);
  }

  /**
   * Viewpoint-adapt {@link #annotation} using {@code stringToJavaExpr}.
   *
   * <p>For example, if the contract is {@code @EnsuresKeyFor(value = "this.field", map = "map")},
   * {@code annotation} is {@code @KeyFor("map")}. This method applies {@code stringToJava} to "map"
   * and returns a new {@code KeyFor} annotation with the result.
   *
   * @param factory used to get {@link DependentTypesHelper}
   * @param stringToJavaExpr function used to convert strings to {@link JavaExpression}s
   * @param errorTree if non-null, where to report any errors that occur when parsing the dependent
   *     type annotation; if null, report no errors
   * @return the viewpoint-adapted annotation, or {@link #annotation} if it is not a dependent type
   *     annotation
   */
  public AnnotationMirror viewpointAdaptDependentTypeAnnotation(
      GenericAnnotatedTypeFactory<?, ?, ?, ?> factory,
      StringToJavaExpression stringToJavaExpr,
      @Nullable Tree errorTree) {
    DependentTypesHelper dependentTypesHelper = factory.getDependentTypesHelper();
    AnnotationMirror standardized =
        dependentTypesHelper.convertAnnotationMirror(stringToJavaExpr, annotation);
    if (standardized == null) {
      return annotation;
    }
    if (errorTree != null) {
      dependentTypesHelper.checkAnnotationForErrorExpressions(standardized, errorTree);
    }
    return standardized;
  }

  /** A precondition contract. */
  public static class Precondition extends Contract {
    /**
     * Create a precondition contract.
     *
     * @param expressionString the Java expression that should have a type qualifier
     * @param annotation the type qualifier that {@code expressionString} should have
     * @param contractAnnotation the precondition annotation that the programmer wrote; used for
     *     diagnostic messages
     */
    public Precondition(
        String expressionString, AnnotationMirror annotation, AnnotationMirror contractAnnotation) {
      super(Kind.PRECONDITION, expressionString, annotation, contractAnnotation);
    }
  }

  /** A postcondition contract. */
  public static class Postcondition extends Contract {
    /**
     * Create a postcondition contract.
     *
     * @param expressionString the Java expression that should have a type qualifier
     * @param annotation the type qualifier that {@code expressionString} should have
     * @param contractAnnotation the postcondition annotation that the programmer wrote; used for
     *     diagnostic messages
     */
    public Postcondition(
        String expressionString, AnnotationMirror annotation, AnnotationMirror contractAnnotation) {
      super(Kind.POSTCONDITION, expressionString, annotation, contractAnnotation);
    }
  }

  /**
   * Represents a conditional postcondition that must be verified by {@code BaseTypeVisitor} or one
   * of its subclasses. Automatically extracted from annotations with meta-annotation
   * {@code @ConditionalPostconditionAnnotation}, such as {@code EnsuresNonNullIf}.
   */
  public static class ConditionalPostcondition extends Contract {

    /**
     * The return value for the annotated method that ensures that the conditional postcondition
     * holds. For example, given
     *
     * <pre>
     * {@code @EnsuresNonNullIf(expression="foo", result=false) boolean method()}
     * </pre>
     *
     * {@code foo} is guaranteed to be {@code @NonNull} after a call to {@code method()} that
     * returns {@code false}.
     */
    public final boolean resultValue;

    /**
     * Create a new conditional postcondition.
     *
     * @param expressionString the Java expression that should have a type qualifier
     * @param annotation the type qualifier that {@code expressionString} should have
     * @param contractAnnotation the postcondition annotation that the programmer wrote; used for
     *     diagnostic messages
     * @param resultValue whether the condition is the method returning true or false
     */
    public ConditionalPostcondition(
        String expressionString,
        AnnotationMirror annotation,
        AnnotationMirror contractAnnotation,
        boolean resultValue) {
      super(Kind.CONDITIONALPOSTCONDITION, expressionString, annotation, contractAnnotation);
      this.resultValue = resultValue;
    }

    @Override
    public boolean equals(@Nullable Object o) {
      return super.equals(o) && resultValue == ((ConditionalPostcondition) o).resultValue;
    }

    @Override
    public int hashCode() {
      return Objects.hash(super.hashCode(), resultValue);
    }

    @Override
    public String toString() {
      String superToString = super.toString();
      return superToString.substring(0, superToString.length() - 1)
          + ", annoResult="
          + resultValue
          + "}";
    }
  }
}
