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.Symbol;
import com.sun.tools.javac.code.TargetType;
import java.util.List;
import javax.lang.model.element.Element;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType;
import org.checkerframework.framework.util.element.ElementAnnotationUtil.UnexpectedAnnotationLocationException;
import org.checkerframework.javacutil.TypesUtils;

/** Apply annotations to a declared type based on its declaration. */
public class TypeDeclarationApplier extends TargetedElementAnnotationApplier {

  public static void apply(
      final AnnotatedTypeMirror type, Element element, AnnotatedTypeFactory typeFactory)
      throws UnexpectedAnnotationLocationException {
    new TypeDeclarationApplier(type, element, typeFactory).extractAndApply();
  }

  /**
   * If a type_index == -1 it means that the index refers to the immediate supertype class of the
   * declaration. There is only ever one of these since Java has no multiple inheritance
   */
  public static final int SUPERCLASS_INDEX = -1;

  /**
   * Returns true if type is an annotated declared type and element is a ClassSymbol.
   *
   * @param type a type
   * @param element an element
   * @return true if type is an annotated declared type and element is a ClassSymbol
   */
  public static boolean accepts(AnnotatedTypeMirror type, Element element) {
    return type instanceof AnnotatedDeclaredType && element instanceof Symbol.ClassSymbol;
  }

  private final AnnotatedTypeFactory typeFactory;
  private final Symbol.ClassSymbol typeSymbol;
  private final AnnotatedDeclaredType declaredType;

  TypeDeclarationApplier(
      AnnotatedTypeMirror type, Element element, AnnotatedTypeFactory typeFactory) {
    super(type, element);
    this.typeFactory = typeFactory;
    this.typeSymbol = (Symbol.ClassSymbol) element;
    this.declaredType = (AnnotatedDeclaredType) type;
  }

  /** The annotated targets. */
  private static final TargetType[] annotatedTargets = new TargetType[] {TargetType.CLASS_EXTENDS};

  @Override
  protected TargetType[] annotatedTargets() {
    return annotatedTargets;
  }

  /** The valid targets. */
  private static final TargetType[] validTargets =
      new TargetType[] {
        TargetType.RESOURCE_VARIABLE,
        TargetType.EXCEPTION_PARAMETER,
        TargetType.NEW,
        TargetType.CAST,
        TargetType.INSTANCEOF,
        TargetType.METHOD_INVOCATION_TYPE_ARGUMENT,
        TargetType.CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT,
        TargetType.METHOD_REFERENCE,
        TargetType.CONSTRUCTOR_REFERENCE,
        TargetType.METHOD_REFERENCE_TYPE_ARGUMENT,
        TargetType.CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT,
        TargetType.CLASS_TYPE_PARAMETER,
        TargetType.CLASS_TYPE_PARAMETER_BOUND
      };

  @Override
  protected TargetType[] validTargets() {
    return validTargets;
  }

  /** All TypeCompounds (annotations) on the ClassSymbol. */
  @Override
  protected Iterable<Attribute.TypeCompound> getRawTypeAttributes() {
    return typeSymbol.getRawTypeAttributes();
  }

  /**
   * While more than just annotations on extends or implements clause are annotated by this class,
   * only these annotations are passed to handleTargeted (as they are the only in the
   * annotatedTargets list). See extractAndApply for type parameters
   *
   * @param extendsAndImplementsAnnos annotations with a TargetType of CLASS_EXTENDS
   */
  @Override
  protected void handleTargeted(List<TypeCompound> extendsAndImplementsAnnos)
      throws UnexpectedAnnotationLocationException {
    if (TypesUtils.isAnonymous(typeSymbol.type)) {
      // If this is an anonymous class, then the annotations after "new" but before the class
      // name are stored as super class annotations. Treat them as annotations on the class.
      for (Attribute.TypeCompound anno : extendsAndImplementsAnnos) {
        if (anno.position.type_index >= SUPERCLASS_INDEX && anno.position.location.isEmpty()) {
          type.addAnnotation(anno);
        }
      }
    }
  }

  /** Adds extends/implements and class annotations to type. Annotates type parameters. */
  @Override
  public void extractAndApply() throws UnexpectedAnnotationLocationException {
    // ensures that we check that there only valid target types on this class, there are no
    // "targeted" locations
    super.extractAndApply();

    // Annotate raw types // TODO: ASK WERNER WHAT THIS MIGHT MEAN?  WHAT ACTUALLY GOES HERE?
    type.addAnnotations(typeSymbol.getAnnotationMirrors());

    ElementAnnotationUtil.applyAllElementAnnotations(
        declaredType.getTypeArguments(), typeSymbol.getTypeParameters(), typeFactory);
  }

  @Override
  protected boolean isAccepted() {
    return accepts(type, element);
  }
}
