package org.checkerframework.framework.ajava;

import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.nodeTypes.NodeWithAnnotations;
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.signature.qual.FullyQualifiedName;
import org.checkerframework.framework.stub.AnnotationFileParser;

/**
 * Moves annotations in a JavaParser AST from declaration position onto the types they correspond
 * to.
 *
 * <p>When parsing a method or field such as {@code @Tainted String myField}, JavaParser puts all
 * annotations on the declaration.
 *
 * <p>For each non-declaration annotation on a method or field declaration, this class moves it to
 * the type position. A non-declaration annotation is one with a {@code TYPE_USE} target but no
 * declaration target.
 */
public class TypeAnnotationMover extends VoidVisitorAdapter<Void> {
  /**
   * Annotations imported by the file, stored as a mapping from names to the TypeElements for the
   * annotations. Contains entries for the simple and fully qualified names of each annotation.
   */
  private final Map<String, TypeElement> allAnnotations;

  /** Element utilities. */
  private final Elements elements;

  /**
   * Constructs a {@code TypeAnnotationMover}.
   *
   * @param allAnnotations the annotations imported by the file, as a mapping from annotation name
   *     to TypeElement. There should be two entries for each annotation: the annotation's simple
   *     name and its fully-qualified name both mapped to its TypeElement.
   * @param elements the Element utilities
   */
  public TypeAnnotationMover(Map<String, TypeElement> allAnnotations, Elements elements) {
    this.allAnnotations = new HashMap<>(allAnnotations);
    this.elements = elements;
  }

  @Override
  public void visit(FieldDeclaration node, Void p) {
    // Use the type of the first declared variable in the field declaration.
    Type type = node.getVariable(0).getType();
    if (!type.isClassOrInterfaceType()) {
      return;
    }

    if (isMultiPartName(type)) {
      return;
    }

    List<AnnotationExpr> annosToMove = getAnnotationsToMove(node, ElementType.FIELD);
    if (annosToMove.isEmpty()) {
      return;
    }

    node.getAnnotations().removeAll(annosToMove);
    annosToMove.forEach(anno -> type.asClassOrInterfaceType().addAnnotation(anno));
  }

  @Override
  public void visit(MethodDeclaration node, Void p) {
    Type type = node.getType();
    if (!type.isClassOrInterfaceType()) {
      return;
    }

    if (isMultiPartName(type)) {
      return;
    }

    List<AnnotationExpr> annosToMove = getAnnotationsToMove(node, ElementType.METHOD);
    if (annosToMove.isEmpty()) {
      return;
    }

    node.getAnnotations().removeAll(annosToMove);
    annosToMove.forEach(anno -> type.asClassOrInterfaceType().addAnnotation(anno));
  }

  /**
   * Given a declaration, returns a List of annotations currently in declaration position that can't
   * possibly be declaration annotations for that type of declaration.
   *
   * @param node a JavaParser node for declaration
   * @param declarationType the type of declaration {@code node} represents; always FIELD or METHOD
   * @return a list of annotations in declaration position that should be on the declaration's type
   */
  private List<AnnotationExpr> getAnnotationsToMove(
      NodeWithAnnotations<?> node, ElementType declarationType) {
    // There are usually no annotations that need to be moved.
    List<AnnotationExpr> annosToMove = new ArrayList<>(0);
    for (AnnotationExpr annotation : node.getAnnotations()) {
      if (!isPossiblyDeclarationAnnotation(annotation, declarationType)) {
        annosToMove.add(annotation);
      }
    }

    return annosToMove;
  }

  /**
   * Returns the TypeElement for an annotation, or null if it cannot be found.
   *
   * @param annotation a JavaParser annotation
   * @return the TypeElement for {@code annotation}, or null if it cannot be found
   */
  private @Nullable TypeElement getAnnotationDeclaration(AnnotationExpr annotation) {
    @SuppressWarnings("signature") // https://tinyurl.com/cfissue/3094
    @FullyQualifiedName String annoNameFq = annotation.getNameAsString();
    TypeElement annoTypeElt = allAnnotations.get(annoNameFq);
    if (annoTypeElt == null) {
      annoTypeElt = elements.getTypeElement(annoNameFq);
      if (annoTypeElt == null) {
        // Not a supported annotation.
        return null;
      }
      AnnotationFileParser.putAllNew(
          allAnnotations,
          AnnotationFileParser.createNameToAnnotationMap(Collections.singletonList(annoTypeElt)));
    }

    return annoTypeElt;
  }

  /**
   * Returns if {@code annotation} could be a declaration annotation for {@code declarationType}.
   * This would be the case if the annotation isn't recognized at all, or if it has no
   * {@code @Target} meta-annotation, or if it has {@code declarationType} as one of its targets.
   *
   * @param annotation a JavaParser annotation expression
   * @param declarationType the declaration type to check if {@code annotation} might be a
   *     declaration annotation for
   * @return true unless {@code annotation} definitely cannot be a declaration annotation for {@code
   *     declarationType}
   */
  private boolean isPossiblyDeclarationAnnotation(
      AnnotationExpr annotation, ElementType declarationType) {
    TypeElement annotationType = getAnnotationDeclaration(annotation);
    if (annotationType == null) {
      return true;
    }

    return isDeclarationAnnotation(annotationType, declarationType);
  }

  /**
   * Returns whether the annotation represented by {@code annotationDeclaration} might be a
   * declaration annotation for {@code declarationType}. This holds if the TypeElement has no
   * {@code @Target} meta-annotation, or if {@code declarationType} is a target of the annotation.
   *
   * @param annotationDeclaration declaration for an annotation
   * @param declarationType the declaration type to check if the annotation might be a declaration
   *     annotation for
   * @return true if {@code annotationDeclaration} contains {@code declarationType} as a target or
   *     doesn't contain {@code ElementType.TYPE_USE} as a target
   */
  private boolean isDeclarationAnnotation(
      TypeElement annotationDeclaration, ElementType declarationType) {
    Target target = annotationDeclaration.getAnnotation(Target.class);
    if (target == null) {
      return true;
    }

    boolean hasTypeUse = false;
    for (ElementType elementType : target.value()) {
      if (elementType == declarationType) {
        return true;
      }

      if (elementType == ElementType.TYPE_USE) {
        hasTypeUse = true;
      }
    }

    if (!hasTypeUse) {
      throw new Error(
          String.format(
              "Annotation %s cannot be used on declaration with type %s",
              annotationDeclaration.getQualifiedName(), declarationType));
    }
    return false;
  }

  /**
   * Returns whether {@code type} has a name containing multiple parts separated by dots, e.g.
   * "java.lang.String" or "Outer.Inner".
   *
   * <p>Annotations should not be moved onto a Type for which this method returns true. A type like
   * {@code @Anno java.lang.String} is illegal since the annotation should go directly next to the
   * rightmost part of the fully qualified name, like {@code java.lang. @Anno String}. So if a file
   * contains a declaration like {@code @Anno java.lang.String myField}, the annotation must belong
   * to the declaration and not the type.
   *
   * <p>If a declaration contains an inner class type like {@code @Anno Outer.Inner myField}, it may
   * be the case that {@code @Anno} belongs to the type {@code Outer}, not the declaration, and
   * should be moved, but it's impossible to distinguish this from the above case using only the
   * JavaParser AST for a file. To be safe, the annotation still shouldn't be moved, but this may
   * lead to suboptimal formatting placing {@code @Anno} on its own line.
   *
   * @param type a JavaParser type node
   * @return true if {@code type} has a multi-part name
   */
  private boolean isMultiPartName(Type type) {
    return type.isClassOrInterfaceType() && type.asClassOrInterfaceType().getScope().isPresent();
  }
}
