package org.checkerframework.framework.flow;

import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Attribute;
import com.sun.tools.javac.code.BoundKind;
import com.sun.tools.javac.code.Symbol.TypeSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.TypeTag;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCAnnotatedType;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCTypeApply;
import com.sun.tools.javac.util.List;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.IntersectionType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.WildcardType;
import org.checkerframework.javacutil.TypeAnnotationUtils;
import org.checkerframework.javacutil.trees.TreeBuilder;

/**
 * The TreeBuilder permits the creation of new AST Trees using the non-public Java compiler API
 * TreeMaker. Initially, it will support construction of desugared Trees required by the CFGBuilder,
 * e.g. the pieces of a desugared enhanced for loop.
 */
public class CFTreeBuilder extends TreeBuilder {

  /**
   * To avoid infinite recursions, record each wildcard that has been converted to a tree. This set
   * is cleared each time {@link #buildAnnotatedType(TypeMirror)} is called.
   */
  private final Set<WildcardType> visitedWildcards = new HashSet<>();

  /**
   * Creates a {@code CFTreeBuilder}.
   *
   * @param env environment
   */
  public CFTreeBuilder(ProcessingEnvironment env) {
    super(env);
  }

  /**
   * Builds an AST Tree representing a type, including AnnotationTrees for its annotations.
   *
   * @param type the type
   * @return a Tree representing the type
   */
  public Tree buildAnnotatedType(TypeMirror type) {
    visitedWildcards.clear();
    return createAnnotatedType(type);
  }

  /**
   * Converts a list of AnnotationMirrors to the a corresponding list of new AnnotationTrees.
   *
   * @param annotations the annotations
   * @return new annotation trees representing the annotations
   */
  private List<JCAnnotation> convertAnnotationMirrorsToAnnotationTrees(
      Collection<? extends AnnotationMirror> annotations) {
    List<JCAnnotation> annotationTrees = List.nil();

    for (AnnotationMirror am : annotations) {
      // TODO: what TypeAnnotationPosition should be used?
      Attribute.TypeCompound typeCompound =
          TypeAnnotationUtils.createTypeCompoundFromAnnotationMirror(
              am, TypeAnnotationUtils.unknownTAPosition(), env);
      JCAnnotation annotationTree = maker.Annotation(typeCompound);
      JCAnnotation typeAnnotationTree =
          maker.TypeAnnotation(annotationTree.getAnnotationType(), annotationTree.getArguments());

      typeAnnotationTree.attribute = typeCompound;

      annotationTrees = annotationTrees.append(typeAnnotationTree);
    }
    return annotationTrees;
  }

  /**
   * Builds an AST Tree representing a type, including AnnotationTrees for its annotations. This
   * internal method differs from the public {@link #buildAnnotatedType(TypeMirror)} only in that it
   * does not reset the list of visited wildcards.
   *
   * @param type the type for which to create a tree
   * @return a Tree representing the type
   */
  private Tree createAnnotatedType(TypeMirror type) {
    // Implementation based on com.sun.tools.javac.tree.TreeMaker.Type

    // Convert the annotations from a set of AnnotationMirrors
    // to a list of AnnotationTrees.
    java.util.List<? extends AnnotationMirror> annotations = type.getAnnotationMirrors();
    List<JCAnnotation> annotationTrees = convertAnnotationMirrorsToAnnotationTrees(annotations);

    // Convert the underlying type from a TypeMirror to an ExpressionTree and combine with the
    // AnnotationTrees to form a ClassTree of kind ANNOTATION_TYPE.
    JCExpression typeTree;
    switch (type.getKind()) {
      case BYTE:
        typeTree = maker.TypeIdent(TypeTag.BYTE);
        break;
      case CHAR:
        typeTree = maker.TypeIdent(TypeTag.CHAR);
        break;
      case SHORT:
        typeTree = maker.TypeIdent(TypeTag.SHORT);
        break;
      case INT:
        typeTree = maker.TypeIdent(TypeTag.INT);
        break;
      case LONG:
        typeTree = maker.TypeIdent(TypeTag.LONG);
        break;
      case FLOAT:
        typeTree = maker.TypeIdent(TypeTag.FLOAT);
        break;
      case DOUBLE:
        typeTree = maker.TypeIdent(TypeTag.DOUBLE);
        break;
      case BOOLEAN:
        typeTree = maker.TypeIdent(TypeTag.BOOLEAN);
        break;
      case VOID:
        typeTree = maker.TypeIdent(TypeTag.VOID);
        break;
      case TYPEVAR:
        // No recursive annotations.
        TypeVariable underlyingTypeVar = (TypeVariable) type;
        typeTree = maker.Ident((TypeSymbol) underlyingTypeVar.asElement());
        break;
      case WILDCARD:
        WildcardType wildcardType = (WildcardType) type;
        boolean visitedBefore = !visitedWildcards.add(wildcardType);
        if (!visitedBefore && wildcardType.getExtendsBound() != null) {
          Tree annotatedExtendsBound = createAnnotatedType(wildcardType.getExtendsBound());
          typeTree =
              maker.Wildcard(
                  maker.TypeBoundKind(BoundKind.EXTENDS), (JCTree) annotatedExtendsBound);
        } else if (!visitedBefore && wildcardType.getSuperBound() != null) {
          Tree annotatedSuperBound = createAnnotatedType(wildcardType.getSuperBound());
          typeTree =
              maker.Wildcard(maker.TypeBoundKind(BoundKind.SUPER), (JCTree) annotatedSuperBound);
        } else {
          typeTree = maker.Wildcard(maker.TypeBoundKind(BoundKind.UNBOUND), null);
        }
        break;
      case INTERSECTION:
        IntersectionType intersectionType = (IntersectionType) type;
        List<JCExpression> components = List.nil();
        for (TypeMirror bound : intersectionType.getBounds()) {
          components = components.append((JCExpression) createAnnotatedType(bound));
        }
        typeTree = maker.TypeIntersection(components);
        break;
        // case UNION:
        // TODO: case UNION similar to INTERSECTION, but write test first.
      case DECLARED:
        typeTree = maker.Type((Type) type);

        if (typeTree instanceof JCTypeApply) {
          // Replace the type parameters with annotated versions.
          DeclaredType annotatedDeclaredType = (DeclaredType) type;
          List<JCExpression> typeArgTrees = List.nil();
          for (TypeMirror arg : annotatedDeclaredType.getTypeArguments()) {
            typeArgTrees = typeArgTrees.append((JCExpression) createAnnotatedType(arg));
          }
          JCExpression clazz = (JCExpression) ((JCTypeApply) typeTree).getType();
          typeTree = maker.TypeApply(clazz, typeArgTrees);
        }
        break;
      case ARRAY:
        ArrayType arrayType = (ArrayType) type;
        Tree componentTree = createAnnotatedType(arrayType.getComponentType());
        typeTree = maker.TypeArray((JCExpression) componentTree);
        break;
      case ERROR:
        typeTree = maker.TypeIdent(TypeTag.ERROR);
        break;
      default:
        assert false : "unexpected type: " + type;
        typeTree = null;
        break;
    }

    typeTree.setType((Type) type);

    if (annotationTrees.isEmpty()) {
      return typeTree;
    }

    JCAnnotatedType annotatedTypeTree = maker.AnnotatedType(annotationTrees, typeTree);
    annotatedTypeTree.setType((Type) type);

    return annotatedTypeTree;
  }
}
