package org.checkerframework.framework.ajava;

import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.nodeTypes.NodeWithAnnotations;
import com.github.javaparser.ast.type.ArrayType;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.type.PrimitiveType;
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.ast.type.TypeParameter;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.type.TypeKind;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable;

/**
 * A visitor that adds all annotations from a {@code AnnotatedTypeMirror} to the corresponding
 * JavaParser type, including nested types like array components.
 *
 * <p>The {@code AnnotatedTypeMirror} is passed as the secondary parameter to the visit methods.
 */
public class AnnotationTransferVisitor extends VoidVisitorAdapter<AnnotatedTypeMirror> {
  @Override
  public void visit(ArrayType target, AnnotatedTypeMirror type) {
    // type can also be a wildcard, in which case don't try to transfer component
    // annotations (there can't be any)
    if (type.getKind() == TypeKind.ARRAY) {
      target.getComponentType().accept(this, ((AnnotatedArrayType) type).getComponentType());
    }
    transferAnnotations(type, target);
  }

  @Override
  public void visit(ClassOrInterfaceType target, AnnotatedTypeMirror type) {
    if (type.getKind() == TypeKind.DECLARED) {
      AnnotatedDeclaredType declaredType = (AnnotatedDeclaredType) type;
      if (target.getTypeArguments().isPresent()) {
        NodeList<Type> types = target.getTypeArguments().get();
        for (int i = 0; i < types.size(); i++) {
          types.get(i).accept(this, declaredType.getTypeArguments().get(i));
        }
      }
    }

    transferAnnotations(type, target);
  }

  @Override
  public void visit(PrimitiveType target, AnnotatedTypeMirror type) {
    transferAnnotations(type, target);
  }

  @Override
  public void visit(TypeParameter target, AnnotatedTypeMirror type) {
    AnnotatedTypeVariable annotatedTypeVar = (AnnotatedTypeVariable) type;
    NodeList<ClassOrInterfaceType> bounds = target.getTypeBound();
    if (bounds.size() == 1) {
      bounds.get(0).accept(this, annotatedTypeVar.getUpperBound());
    }
  }

  /**
   * Transfers annotations from {@code annotatedType} to {@code target}. Does nothing if {@code
   * annotatedType} is null.
   *
   * @param annotatedType type with annotations to transfer
   * @param target a JavaParser node representing the type to transfer annotations to
   */
  private void transferAnnotations(
      @Nullable AnnotatedTypeMirror annotatedType, NodeWithAnnotations<?> target) {
    if (annotatedType == null) {
      return;
    }

    for (AnnotationMirror annotation : annotatedType.getPrimaryAnnotations()) {
      AnnotationExpr convertedAnnotation =
          AnnotationMirrorToAnnotationExprConversion.annotationMirrorToAnnotationExpr(annotation);
      target.addAnnotation(convertedAnnotation);
    }
  }
}
