package org.checkerframework.framework.util.element;

import com.sun.tools.javac.code.Attribute;
import com.sun.tools.javac.code.Attribute.TypeCompound;
import com.sun.tools.javac.code.TargetType;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import javax.lang.model.element.Element;
import javax.lang.model.type.TypeKind;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.util.element.ElementAnnotationUtil.UnexpectedAnnotationLocationException;
import org.checkerframework.javacutil.BugInCF;
import org.plumelib.util.StringsPlume;

/**
 * TargetedElementAnnotationApplier filters annotations for an element into 3 groups. TARGETED
 * annotations are those we wish to apply in this ElementAnnotationApplier. VALID annotations are
 * those that are valid on the current element (or its enclosure, see getRawTypeAttributes) but
 * should not be applied to the given type. Invalid annotations are those that should NEVER appear
 * for the given element. Invalid annotations are reported as errors by default in the handleInvalid
 * method. See method extractAndApply. Please read getRawTypeAttributes for an idea of what types of
 * annotations may be encountered by this ElementAnnotationApplier.
 *
 * <p>Note: Subtypes of this class likely want to implement the handleTargeted and handleValid
 * methods though they have default empty implementations for brevity.
 */
abstract class TargetedElementAnnotationApplier {
  /**
   * Three annotation types that may be encountered when calling getRawTypeAttributes. see sift().
   */
  static enum TargetClass {
    TARGETED,
    VALID,
    INVALID
  }

  /** The type to which we wish to apply annotations. */
  protected final AnnotatedTypeMirror type;

  /** An Element that type represents. */
  protected final Element element;

  /**
   * Returns the TargetTypes that identify annotations we wish to apply with this object. Any
   * annotations that have these target types will be passed to handleTargeted.
   *
   * @return the TargetTypes that identify annotations we wish to apply with this object. Any
   *     annotations that have these target types will be passed to handleTargeted
   */
  protected abstract TargetType[] annotatedTargets();

  /**
   * Returns the TargetTypes that identify annotations that are valid but we wish to ignore. Any
   * annotations that have these target types will be passed to handleValid, providing they aren't
   * also in annotatedTargets.
   *
   * @return the TargetTypes that identify annotations that are valid but we wish to ignore
   */
  protected abstract TargetType[] validTargets();

  /**
   * Annotations on elements are represented as Attribute.TypeCompounds ( a subtype of
   * AnnotationMirror) that are usually accessed through a getRawTypeAttributes method on the
   * element.
   *
   * <p>In Java 8 and later these annotations are generally contained by elements to which they
   * apply. However, in earlier versions of Java many of these annotations are handled by either the
   * enclosing method, e.g. parameters and method type parameters, or enclosing class, e.g. class
   * type parameters. Therefore, many annotations are addressed by first getting all annotations on
   * a method or class and the picking out only the ones we wish to target (see extractAndApply).
   *
   * @return the annotations that we MAY wish to apply to the given type
   */
  protected abstract Iterable<Attribute.TypeCompound> getRawTypeAttributes();

  /**
   * Tests element/type fields to ensure that this TargetedElementAnnotationApplier is valid for
   * this element/type pair.
   *
   * @return true if the type/element members are handled by this class false otherwise
   */
  protected abstract boolean isAccepted();

  /**
   * @param type the type to annotate
   * @param element an element identifying type
   */
  TargetedElementAnnotationApplier(AnnotatedTypeMirror type, Element element) {
    this.type = type;
    this.element = element;
  }

  /**
   * This method should apply all annotations that are handled by this object.
   *
   * @param targeted the list of annotations that were returned by getRawTypeAttributes and had a
   *     TargetType contained by annotatedTargets
   */
  protected abstract void handleTargeted(List<TypeCompound> targeted)
      throws UnexpectedAnnotationLocationException;

  /**
   * The default implementation of this method does nothing.
   *
   * @param valid the list of annotations that were returned by getRawTypeAttributes and had a
   *     TargetType contained by valid and NOT annotatedTargets
   */
  protected void handleValid(List<Attribute.TypeCompound> valid) {}

  /**
   * This implementation reports all invalid annotations as errors.
   *
   * @param invalid the list of annotations that were returned by getRawTypeAttributes and were not
   *     handled by handleTargeted or handleValid
   */
  protected void handleInvalid(List<Attribute.TypeCompound> invalid) {
    List<Attribute.TypeCompound> remaining = new ArrayList<>(invalid.size());
    for (Attribute.TypeCompound tc : invalid) {
      if (tc.getAnnotationType().getKind() != TypeKind.ERROR) {
        // Filter out annotations that have an error type. javac will
        // already have raised an error for them.
        remaining.add(tc);
      }
    }
    if (!remaining.isEmpty()) {
      StringJoiner msg = new StringJoiner(System.lineSeparator());
      msg.add("handleInvalid(this=" + this.getClass().getName() + "):");
      msg.add("Invalid variable and element passed to extractAndApply; type: " + type);
      String elementInfoPrefix =
          "  element: " + element + " (kind: " + element.getKind() + "), invalid annotations: ";
      StringJoiner remainingInfo = new StringJoiner(", ", elementInfoPrefix, "");
      for (Attribute.TypeCompound r : remaining) {
        remainingInfo.add(r.toString() + " (" + r.position + ")");
      }
      msg.add(remainingInfo.toString());
      msg.add("Targeted annotations: " + StringsPlume.join(", ", annotatedTargets()));
      msg.add("Valid annotations: " + StringsPlume.join(", ", validTargets()));

      throw new BugInCF(msg.toString());
    }
  }

  /**
   * Separate the input annotations into a Map of TargetClass (TARGETED, VALID, INVALID) to the
   * annotations that fall into each of those categories.
   *
   * @param typeCompounds annotations to sift through, should be those returned by
   *     getRawTypeAttributes
   * @return a {@literal Map<TargetClass => Annotations>.}
   */
  protected Map<TargetClass, List<Attribute.TypeCompound>> sift(
      Iterable<Attribute.TypeCompound> typeCompounds) {

    Map<TargetClass, List<Attribute.TypeCompound>> targetClassToCompound =
        new EnumMap<>(TargetClass.class);
    for (TargetClass targetClass : TargetClass.values()) {
      targetClassToCompound.put(targetClass, new ArrayList<>());
    }

    for (Attribute.TypeCompound typeCompound : typeCompounds) {
      TargetType typeCompoundTarget = typeCompound.position.type;
      List<Attribute.TypeCompound> destList;

      if (ElementAnnotationUtil.contains(typeCompoundTarget, annotatedTargets())) {
        destList = targetClassToCompound.get(TargetClass.TARGETED);

      } else if (ElementAnnotationUtil.contains(typeCompoundTarget, validTargets())) {
        destList = targetClassToCompound.get(TargetClass.VALID);

      } else {
        destList = targetClassToCompound.get(TargetClass.INVALID);
      }

      destList.add(typeCompound);
    }

    return targetClassToCompound;
  }

  /**
   * Reads the list of annotations that apply to this element (see getRawTypeAttributes). Sifts them
   * into three groups (TARGETED, INVALID, VALID) and then calls the appropriate handle method on
   * them. The handleTargeted method should apply all annotations that are handled by this object.
   *
   * <p>This method will throw a runtime exception if isAccepted returns false.
   */
  public void extractAndApply() throws UnexpectedAnnotationLocationException {
    if (!isAccepted()) {
      throw new BugInCF(
          "LocalVariableExtractor.extractAndApply: "
              + "Invalid variable and element passed to "
              + this.getClass().getName()
              + "::extractAndApply ("
              + type
              + ", "
              + element);
    }

    Map<TargetClass, List<Attribute.TypeCompound>> targetClassToAnno = sift(getRawTypeAttributes());

    handleInvalid(targetClassToAnno.get(TargetClass.INVALID));
    handleValid(targetClassToAnno.get(TargetClass.VALID));
    handleTargeted(targetClassToAnno.get(TargetClass.TARGETED));
  }
}
