package org.checkerframework.common.wholeprograminference;

import com.sun.source.tree.ClassTree;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import org.checkerframework.checker.index.qual.Positive;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.dataflow.analysis.Analysis;
import org.checkerframework.framework.qual.TypeUseLocation;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.javacutil.AnnotationMirrorSet;

/**
 * Stores annotations from whole-program inference. For a given location such as a field or method,
 * an object can be obtained containing the inferred annotations for that object.
 *
 * <p>Also writes stored annotations to storage files. The specific format depends on the
 * implementation.
 *
 * @param <T> the type used by the storage to store annotations. The methods {@link
 *     #atmFromStorageLocation} and {@link #updateStorageLocationFromAtm} can be used to manipulate
 *     a storage location.
 */
public interface WholeProgramInferenceStorage<T> {
  /**
   * Returns the file corresponding to the given element. This may side-effect the storage to load
   * the file if it hasn't been read yet.
   *
   * @param elt an element
   * @return the path to the file where inference results for the element will be written
   */
  public String getFileForElement(Element elt);

  /**
   * Given an ExecutableElement in a compilation unit that has already been read into storage,
   * returns whether there exists a stored method matching {@code elt}.
   *
   * <p>An implementation is permitted to return false if {@code elt} represents a method that was
   * synthetically added by javac, such as zero-argument constructors or valueOf(String) methods for
   * enum types.
   *
   * @param methodElt a method or constructor Element
   * @return true if the storage has a method corresponding to {@code elt}
   */
  public boolean hasStorageLocationForMethod(ExecutableElement methodElt);

  /**
   * Get the annotations for a formal parameter type.
   *
   * @param methodElt the method or constructor Element
   * @param index_1based the parameter index (1-based)
   * @param paramATM the parameter type
   * @param ve the parameter variable
   * @param atypeFactory the type factory
   * @return the annotations for a formal parameter type
   */
  public T getParameterAnnotations(
      ExecutableElement methodElt,
      @Positive int index_1based,
      AnnotatedTypeMirror paramATM,
      VariableElement ve,
      AnnotatedTypeFactory atypeFactory);

  /**
   * Get the annotations for the receiver type.
   *
   * @param methodElt the method or constructor Element
   * @param paramATM the receiver type
   * @param atypeFactory the type factory
   * @return the annotations for the receiver type
   */
  public T getReceiverAnnotations(
      ExecutableElement methodElt, AnnotatedTypeMirror paramATM, AnnotatedTypeFactory atypeFactory);

  /**
   * Get the annotations for the return type.
   *
   * @param methodElt the method or constructor Element
   * @param atm the return type
   * @param atypeFactory the type factory
   * @return the annotations for the return type
   */
  public T getReturnAnnotations(
      ExecutableElement methodElt, AnnotatedTypeMirror atm, AnnotatedTypeFactory atypeFactory);

  /**
   * Get the annotations for a field type.
   *
   * @param element the element for the field
   * @param fieldName the simple field name
   * @param lhsATM the field type
   * @param atypeFactory the annotated type factory
   * @return the annotations for a field type
   */
  public T getFieldAnnotations(
      Element element,
      String fieldName,
      AnnotatedTypeMirror lhsATM,
      AnnotatedTypeFactory atypeFactory);

  /**
   * Returns the pre- or postcondition annotations for an expression. The format of the expression
   * is the same as a programmer would write in a {@link
   * org.checkerframework.framework.qual.RequiresQualifier} or {@link
   * org.checkerframework.framework.qual.EnsuresQualifier} annotation.
   *
   * <p>This method may return null if the given expression is not a supported expression type.
   * Currently, the supported expression types are: fields of "this" (e.g. "this.f", pre- and
   * postconditions), "this" (postconditions only), and method parameters (e.g. "#1", "#2",
   * postconditions only).
   *
   * @param className the class that contains the method, for diagnostics only
   * @param preOrPost whether to get the precondition or postcondition
   * @param methodElement the method
   * @param expression the expression
   * @param declaredType the declared type of the expression
   * @param atypeFactory the type factory
   * @return the pre- or postcondition annotations for an expression, or null if the given
   *     expression is not a supported expression type
   */
  public @Nullable T getPreOrPostconditions(
      String className,
      Analysis.BeforeOrAfter preOrPost,
      ExecutableElement methodElement,
      String expression,
      AnnotatedTypeMirror declaredType,
      AnnotatedTypeFactory atypeFactory);

  /**
   * Updates a method to add a declaration annotation.
   *
   * @param methodElt the method to annotate
   * @param anno the declaration annotation to add to the method
   * @return true if {@code anno} is a new declaration annotation for {@code methodElt}, false
   *     otherwise
   */
  public boolean addMethodDeclarationAnnotation(ExecutableElement methodElt, AnnotationMirror anno);

  /**
   * Updates a field to add a declaration annotation.
   *
   * @param fieldElt the field
   * @param anno the declaration annotation to add to the field
   * @return true if {@code anno} is a new declaration annotation for {@code fieldElt}, false
   *     otherwise
   */
  public boolean addFieldDeclarationAnnotation(VariableElement fieldElt, AnnotationMirror anno);

  /**
   * Adds a declaration annotation to a formal parameter.
   *
   * @param methodElt the method whose formal parameter will be annotated
   * @param index_1based the index of the parameter (1-indexed)
   * @param anno the annotation to add
   * @return true if {@code anno} is a new declaration annotation for {@code methodElt}, false
   *     otherwise
   */
  public boolean addDeclarationAnnotationToFormalParameter(
      ExecutableElement methodElt, @Positive int index_1based, AnnotationMirror anno);

  /**
   * Adds an annotation to a class declaration.
   *
   * @param classElt the class declaration to annotate
   * @param anno the annotation to add
   * @return true if {@code anno} is a new declaration annotation for {@code classElt}, false
   *     otherwise
   */
  public boolean addClassDeclarationAnnotation(TypeElement classElt, AnnotationMirror anno);

  /**
   * Return the list of declaration annotations inferred on the given method so far in this round of
   * WPI.
   *
   * @param elt a method
   * @return the declaration annotations inferred on elt so far (may be empty)
   */
  AnnotationMirrorSet getMethodDeclarationAnnotations(ExecutableElement elt);

  /**
   * Removes the given annotation from the given method element's inferred declaration annotation.
   * If the given annotation was not in the list of inferred declaration annotations on the given
   * method, calling this method is a no-op.
   *
   * @param methodElt a method element
   * @param anno a declaration annotation to remove
   * @return true if the annotation was successfully removed, false if not (e.g., if it wasn't
   *     present)
   */
  boolean removeMethodDeclarationAnnotation(ExecutableElement methodElt, AnnotationMirror anno);

  /**
   * Obtain the type from a storage location.
   *
   * @param typeMirror the underlying type for the result
   * @param storageLocation the storage location from which to obtain annotations
   * @return an annotated type mirror with underlying type {@code typeMirror} and annotations from
   *     {@code storageLocation}
   */
  public AnnotatedTypeMirror atmFromStorageLocation(TypeMirror typeMirror, T storageLocation);

  /**
   * Updates a storage location to have the annotations of the given {@code AnnotatedTypeMirror}.
   * Annotations in the original set that should be ignored are not added to the resulting set. If
   * {@code ignoreIfAnnotated} is true, doesn't add annotations for locations with explicit
   * annotations in source code.
   *
   * <p>This method removes from the storage location all annotations supported by the
   * AnnotatedTypeFactory before inserting new ones. It is assumed that every time this method is
   * called, the new {@code AnnotatedTypeMirror} has a better type estimate for the given location.
   * Therefore, it is not a problem to remove all annotations before inserting the new annotations.
   *
   * <p>The {@code update*} methods in {@link WholeProgramInference} perform LUB. This one just does
   * replacement. (Thus, the naming may be a bit confusing.)
   *
   * @param newATM the type whose annotations will be added to the {@code AnnotatedTypeMirror}
   * @param curATM the annotations currently stored at the location, used to check if the element
   *     that will be updated has explicit annotations in source code
   * @param storageLocationToUpdate the storage location that will be updated
   * @param defLoc the location where the annotation will be added
   * @param ignoreIfAnnotated if true, don't update any type that is explicitly annotated in the
   *     source code
   */
  public void updateStorageLocationFromAtm(
      AnnotatedTypeMirror newATM,
      AnnotatedTypeMirror curATM,
      T storageLocationToUpdate,
      TypeUseLocation defLoc,
      boolean ignoreIfAnnotated);

  /**
   * Writes the inferred results to a file. Ideally, it should be called at the end of the
   * type-checking process. In practice, it is called after each class, because we don't know which
   * class will be the last one in the type-checking process.
   *
   * @param outputFormat the file format in which to write the results
   * @param checker the checker from which this method is called, for naming annotation files
   */
  public void writeResultsToFile(
      WholeProgramInference.OutputFormat outputFormat, BaseTypeChecker checker);

  /**
   * Indicates that inferred annotations for the file at {@code path} have changed since last
   * written. This causes output files for {@code path} to be written out next time {@link
   * #writeResultsToFile} is called.
   *
   * @param path path to the file with annotations that have been modified
   */
  public void setFileModified(String path);

  /**
   * Performs any preparation required for inference on the elements of a class. Should be called on
   * each top-level class declaration in a compilation unit before processing it.
   *
   * @param classTree the class to preprocess
   */
  void preprocessClassTree(ClassTree classTree);
}
