package org.checkerframework.framework.type;

import com.sun.tools.javac.code.Symtab;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.util.Context;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.IntersectionType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.WildcardType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedIntersectionType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedNoType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedNullType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedPrimitiveType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedUnionType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedWildcardType;
import org.checkerframework.framework.type.visitor.AnnotatedTypeVisitor;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.Pair;
import org.checkerframework.javacutil.PluginUtil;
import org.checkerframework.javacutil.TypeAnnotationUtils;
import org.checkerframework.javacutil.TypesUtils;

/**
 * BoundsInitializer creates AnnotatedTypeMirrors (without annotations) for the bounds of type
 * variables and wildcards. Its static helper methods are called from AnnotatedTypeMirror. When an
 * initializer method is called for a particular bound, the entirety of that bound, including
 * circular references, will be created.
 */
public class BoundsInitializer {
    // ============================================================================================
    // Static helper methods called from AnnotatedTypeMirror to initialize bounds of wildcards or
    // type variables
    // ============================================================================================

    /**
     * Initializes the type arguments of {@code declaredType}. The upper bound of unbound wildcards
     * is set to the upper bound of the type parameter for which it is an argument. If {@code
     * declaredType} is raw, then the type arguments are uninferred wildcards.
     *
     * @param declaredType type whose arguments are initialized
     */
    public static void initializeTypeArgs(AnnotatedDeclaredType declaredType) {
        final DeclaredType actualType = (DeclaredType) declaredType.actualType;
        if (actualType.getTypeArguments().isEmpty() && !declaredType.wasRaw()) {
            // No type arguments to infer.
            return;
        }

        final TypeElement typeElement =
                (TypeElement) declaredType.atypeFactory.types.asElement(actualType);
        final List<AnnotatedTypeMirror> typeArgs = new ArrayList<>();

        // Create AnnotatedTypeMirror for each type argument and store them in the typeArgsMap.
        // Take un-annotated type variables as the key for this map.
        Map<TypeVariable, AnnotatedTypeMirror> typeArgMap = new HashMap<>();
        for (int i = 0; i < typeElement.getTypeParameters().size(); i++) {
            TypeMirror javaTypeArg;
            if (declaredType.wasRaw()) {
                TypeVariable typeVariable =
                        (TypeVariable) typeElement.getTypeParameters().get(i).asType();
                javaTypeArg = getUpperBoundAsWildcard(typeVariable, declaredType.atypeFactory);
            } else {
                javaTypeArg = declaredType.getUnderlyingType().getTypeArguments().get(i);
            }

            final AnnotatedTypeMirror typeArg =
                    AnnotatedTypeMirror.createType(javaTypeArg, declaredType.atypeFactory, false);
            if (typeArg.getKind() == TypeKind.WILDCARD) {
                AnnotatedWildcardType wildcardType = (AnnotatedWildcardType) typeArg;
                wildcardType.setTypeVariable(typeElement.getTypeParameters().get(i));
                if (declaredType.wasRaw()) {
                    wildcardType.setUninferredTypeArgument();
                }
            }
            typeArgs.add(typeArg);

            // Add mapping from type parameter to the annotated type argument.
            TypeVariable key =
                    (TypeVariable)
                            TypeAnnotationUtils.unannotatedType(
                                    typeElement.getTypeParameters().get(i).asType());
            typeArgMap.put(key, typeArg);

            if (javaTypeArg.getKind() == TypeKind.TYPEVAR) {
                // Add mapping from Java type argument to the annotated type argument.
                key = (TypeVariable) TypeAnnotationUtils.unannotatedType(javaTypeArg);
                typeArgMap.put(key, typeArg);
            }
        }

        // Initialize type argument bounds using the typeArgsMap.
        for (AnnotatedTypeMirror typeArg : typeArgs) {
            switch (typeArg.getKind()) {
                case WILDCARD:
                    AnnotatedWildcardType wildcardType = (AnnotatedWildcardType) typeArg;
                    initializeExtendsBound(wildcardType, typeArgMap);
                    initializeSuperBound(wildcardType, typeArgMap);
                    break;
                case TYPEVAR:
                    initializeBounds((AnnotatedTypeVariable) typeArg, typeArgMap);
                    break;
                default:
                    // do nothing
            }
        }
        declaredType.typeArgs = Collections.unmodifiableList(typeArgs);
    }

    /**
     * Returns a wildcard whose upper bound is the same as {@code typeVariable}. If the upper bound
     * is an intersection, then this method returns an unbound wildcard.
     */
    private static WildcardType getUpperBoundAsWildcard(
            TypeVariable typeVariable, AnnotatedTypeFactory factory) {
        TypeMirror upperBound = typeVariable.getUpperBound();
        switch (upperBound.getKind()) {
            case ARRAY:
            case DECLARED:
            case TYPEVAR:
                return factory.types.getWildcardType(upperBound, null);
            case INTERSECTION:
                // Can't create a wildcard with an intersection as the upper bound, so use
                // an unbound wildcard instead.  The extends bound of the
                // AnnotatedWildcardType will be initialized properly by this class.
                return factory.types.getWildcardType(null, null);
            default:
                throw new BugInCF(
                        "Unexpected upper bound kind: %s type: %s",
                        upperBound.getKind(), upperBound);
        }
    }

    /**
     * Create the entire lower bound and upper bound, with no missing information, for typeVar. If a
     * typeVar is recursive the appropriate cycles will be introduced in the type
     *
     * @param typeVar the type variable whose lower bound is being initialized
     */
    public static void initializeBounds(final AnnotatedTypeVariable typeVar) {
        initializeBounds(typeVar, null);
    }

    /**
     * Create the entire lower bound and upper bound, with no missing information, for typeVar. If a
     * typeVar is recursive the appropriate cycles will be introduced in the type
     *
     * @param typeVar the type variable whose lower bound is being initialized
     * @param map a mapping of type parameters to type arguments. May be null.
     */
    private static void initializeBounds(
            final AnnotatedTypeVariable typeVar, Map<TypeVariable, AnnotatedTypeMirror> map) {
        final Set<AnnotationMirror> annos = saveAnnotations(typeVar);

        InitializerVisitor visitor =
                new InitializerVisitor(new TypeVariableStructure(typeVar), map);
        visitor.initializeLowerBound(typeVar);
        visitor.resolveTypeVarReferences(typeVar);

        InitializerVisitor visitor2 =
                new InitializerVisitor(new TypeVariableStructure(typeVar), map);
        visitor2.initializeUpperBound(typeVar);
        visitor2.resolveTypeVarReferences(typeVar);

        restoreAnnotations(typeVar, annos);
    }

    /**
     * If we are initializing a type variable with a primary annotation than we should first
     * initialize it as if it were a declaration (i.e. as if it had no primary annotations) and then
     * apply the primary annotations. We do this so that when we make copies of the original type to
     * represent recursive references the recursive references don't have the primary annotation.
     *
     * <pre>{@code
     * e.g.   given the declaration {@code <E extends List<E>>}
     *        if we do not do this, the NonNull on the use @NonNull E
     *        would be copied to the primary annotation on E in the bound {@code List<E>}
     *        i.e. the use would be {@code <@NonNull E extends @NonNull List<@NonNull E>>}
     *             rather than      {@code <@NonNull E extends @NonNull List<E>>}
     * }</pre>
     */
    private static Set<AnnotationMirror> saveAnnotations(final AnnotatedTypeMirror type) {
        if (!type.getAnnotationsField().isEmpty()) {
            final Set<AnnotationMirror> annos = new HashSet<>(type.getAnnotations());
            type.clearAnnotations();
            return annos;
        }

        return null;
    }

    private static void restoreAnnotations(
            final AnnotatedTypeMirror type, final Set<AnnotationMirror> annos) {
        if (annos != null) {
            type.addAnnotations(annos);
        }
    }

    /**
     * Create the entire super bound, with no missing information, for wildcard. If a wildcard is
     * recursive the appropriate cycles will be introduced in the type
     *
     * @param wildcard the wildcard whose lower bound is being initialized
     */
    public static void initializeSuperBound(final AnnotatedWildcardType wildcard) {
        initializeSuperBound(wildcard, null);
    }

    /**
     * Create the entire super bound, with no missing information, for wildcard. If a wildcard is
     * recursive the appropriate cycles will be introduced in the type
     *
     * @param wildcard the wildcard whose lower bound is being initialized
     * @param map a mapping of type parameters to type arguments. May be null.
     */
    private static void initializeSuperBound(
            final AnnotatedWildcardType wildcard, Map<TypeVariable, AnnotatedTypeMirror> map) {
        final Set<AnnotationMirror> annos = saveAnnotations(wildcard);

        InitializerVisitor visitor = new InitializerVisitor(new RecursiveTypeStructure(), map);
        visitor.initializeSuperBound(wildcard);
        visitor.resolveTypeVarReferences(wildcard);

        restoreAnnotations(wildcard, annos);
    }

    /**
     * Create the entire extends bound, with no missing information, for wildcard. If a wildcard is
     * recursive the appropriate cycles will be introduced in the type
     *
     * @param wildcard the wildcard whose extends bound is being initialized
     */
    public static void initializeExtendsBound(final AnnotatedWildcardType wildcard) {
        initializeExtendsBound(wildcard, null);
    }

    /**
     * Create the entire extends bound, with no missing information, for wildcard. If a wildcard is
     * recursive the appropriate cycles will be introduced in the type
     *
     * @param wildcard the wildcard whose extends bound is being initialized
     * @param map a mapping of type parameters to type arguments. May be null.
     */
    private static void initializeExtendsBound(
            final AnnotatedWildcardType wildcard, Map<TypeVariable, AnnotatedTypeMirror> map) {
        final Set<AnnotationMirror> annos = saveAnnotations(wildcard);
        InitializerVisitor visitor = new InitializerVisitor(new RecursiveTypeStructure(), map);
        visitor.initializeExtendsBound(wildcard);
        visitor.resolveTypeVarReferences(wildcard);
        restoreAnnotations(wildcard, annos);
    }

    // ============================================================================================
    // Classes and methods used to make the above static helper methods work
    // ============================================================================================

    /**
     * Creates the AnnotatedTypeMirrors (without annotations) for the bounds of all type variables
     * and wildcards in a given type. If the type is recursive, {@code T extends Comparable<T>},
     * then all references to the same type variable are references to the same AnnotatedTypeMirror.
     */
    private static class InitializerVisitor implements AnnotatedTypeVisitor<Void, Void> {
        /**
         * The {@link RecursiveTypeStructure} corresponding to the first wildcard or type variable
         * bound initialization that kicked this visitation off.
         */
        private final RecursiveTypeStructure topLevelStructure;

        /**
         * The {@link RecursiveTypeStructure} corresponding to the wildcard or type variable that is
         * currently being visited.
         */
        private RecursiveTypeStructure currentStructure;

        /** A mapping from TypeVariable to its {@link TypeVariableStructure}. */
        private final Map<TypeVariable, TypeVariableStructure> typeVarToStructure = new HashMap<>();

        /**
         * A mapping from WildcardType to its AnnotatedWildcardType. The first time this visitor
         * encounters a wildcard it creates an annotated type and adds it to this map. The next time
         * the wilcard is encounter, the annotated type in this map is returned.
         */
        private final Map<WildcardType, AnnotatedWildcardType> wildcards = new HashMap<>();

        /**
         * A mapping from IntersectionType to its AnnotatedIntersectionType. The first time this
         * visitor encounters an intersection it creates an annotated type and adds it to this map.
         * The next time the intersection is encounter, the annotated type in this map is returned.
         */
        private final Map<IntersectionType, AnnotatedIntersectionType> intersections =
                new HashMap<>();

        /**
         * Mapping from TypeVariable to AnnotatedTypeMirror. The annotated type mirror should be
         * used for any use of the type variable rather than creating and initializing a new
         * annotated type. This is used for type arguments that have already been initialized
         * outside of this visitor.
         */
        private final Map<TypeVariable, AnnotatedTypeMirror> typevars;

        /**
         * Creates an InitializerVisitor.
         *
         * @param recursiveTypeStructure structure for the type being initialized
         * @param typevars a mapping from type variable to annotated types that have already been
         *     initialized
         */
        public InitializerVisitor(
                RecursiveTypeStructure recursiveTypeStructure,
                Map<TypeVariable, AnnotatedTypeMirror> typevars) {
            this.topLevelStructure = recursiveTypeStructure;
            this.currentStructure = recursiveTypeStructure;
            if (typevars != null) {
                this.typevars = typevars;
            } else {
                this.typevars = new HashMap<>();
            }
            if (recursiveTypeStructure instanceof TypeVariableStructure) {
                TypeVariableStructure typeVarStruct =
                        (TypeVariableStructure) recursiveTypeStructure;
                typeVarToStructure.put(typeVarStruct.typeVar, typeVarStruct);
            }
        }

        // ----------------------------------------------------------------------------------------
        // Visit methods that keep track of the path traversed through type variable bounds, and the
        // wildcards/intersections that have been encountered.
        // ----------------------------------------------------------------------------------------

        @Override
        public Void visit(AnnotatedTypeMirror type) {
            type.accept(this, null);
            return null;
        }

        @Override
        public Void visit(AnnotatedTypeMirror type, Void aVoid) {
            visit(type);
            return null;
        }

        @Override
        public Void visitDeclared(AnnotatedDeclaredType type, Void aVoid) {
            initializeTypeArgs(type);
            if (type.enclosingType != null) {
                TypePathNode node = currentStructure.addPathNode(new EnclosingTypeNode());
                visit(type.enclosingType);
                currentStructure.removePathNode(node);
            }
            return null;
        }

        @Override
        public Void visitIntersection(AnnotatedIntersectionType type, Void aVoid) {

            if (intersections.containsKey(type.getUnderlyingType())) {
                return null;
            }

            intersections.put(type.getUnderlyingType(), type);

            List<AnnotatedDeclaredType> supertypes = type.directSuperTypes();
            for (int i = 0; i < supertypes.size(); i++) {
                AnnotatedDeclaredType supertype = supertypes.get(i);
                TypePathNode node = currentStructure.addPathNode(new IntersectionBoundNode(i));
                visit(supertype);
                currentStructure.removePathNode(node);
            }
            return null;
        }

        @Override
        public Void visitUnion(AnnotatedUnionType type, Void aVoid) {

            List<AnnotatedDeclaredType> alts = type.getAlternatives();
            for (int i = 0; i < alts.size(); i++) {
                AnnotatedDeclaredType alt = alts.get(i);
                TypePathNode node = currentStructure.addPathNode(new AlternativeTypeNode(i));
                visit(alt);
                currentStructure.removePathNode(node);
            }
            return null;
        }

        @Override
        public Void visitArray(AnnotatedArrayType type, Void aVoid) {
            if (!TypesUtils.isPrimitive(type.getComponentType().getUnderlyingType())) {
                // Only recur on component type if it's not a primitive.
                // Array component types are the only place a primitive is allowed in bounds
                TypePathNode componentNode = currentStructure.addPathNode(new ArrayComponentNode());
                type.setComponentType(getOrVisit(type.getComponentType()));
                currentStructure.removePathNode(componentNode);
            }
            return null;
        }

        @Override
        public Void visitTypeVariable(AnnotatedTypeVariable type, Void aVoid) {
            this.currentStructure.addTypeVar(type.getUnderlyingType());
            if (typeVarToStructure.containsKey(type.getUnderlyingType())) {
                return null;
            }
            TypeVariableStructure typeVarStruct = new TypeVariableStructure(type);
            typeVarToStructure.put(type.getUnderlyingType(), typeVarStruct);
            RecursiveTypeStructure parentStructure = this.currentStructure;

            this.currentStructure = typeVarStruct;
            initializeUpperBound(type);
            initializeLowerBound(type);
            this.currentStructure = parentStructure;

            return null;
        }

        @Override
        public Void visitNull(AnnotatedNullType type, Void aVoid) {
            return null;
        }

        @Override
        public Void visitWildcard(AnnotatedWildcardType wildcard, Void aVoid) {
            if (wildcard.getSuperBoundField() == null) {
                initializeSuperBound(wildcard);

            } else {
                throw new BugInCF(
                        "Wildcard super field should not be initialized:%n"
                                + "wildcard=%s%n"
                                + "currentStructure=%s%n",
                        wildcard, currentStructure);
            }

            if (wildcard.getExtendsBoundField() == null) {
                initializeExtendsBound(wildcard);
            } else {
                throw new BugInCF(
                        "Wildcard extends field should not be initialized:%n"
                                + "wildcard=%s%n"
                                + "currentStructure=%s%n",
                        wildcard, currentStructure);
            }

            return null;
        }

        @Override
        public Void visitPrimitive(AnnotatedPrimitiveType type, Void aVoid) {
            throw new BugInCF("Unexpected AnnotatedPrimitiveType.");
        }

        @Override
        public Void visitNoType(AnnotatedNoType type, Void aVoid) {
            throw new BugInCF("Unexpected AnnotatedNoType.");
        }

        @Override
        public Void visitExecutable(AnnotatedExecutableType type, Void aVoid) {
            throw new BugInCF("Unexpected AnnotatedExecutableType.");
        }

        /**
         * If the underlying type of (@code type} has been visited before, return the previous
         * AnnotatedTypeMirror. Otherwise, visit {@code type} and return it.
         *
         * @param type type to visit
         * @return {@code type} or an AnnotatedTypeMirror with the same underlying type that was
         *     previously visited.
         */
        public AnnotatedTypeMirror getOrVisit(AnnotatedTypeMirror type) {
            switch (type.getKind()) {
                case WILDCARD:
                    AnnotatedWildcardType wildcard = (AnnotatedWildcardType) type;
                    if (wildcards.containsKey(wildcard.getUnderlyingType())) {
                        return wildcards.get(wildcard.getUnderlyingType());
                    }
                    break;
                case INTERSECTION:
                    if (intersections.containsKey(type.getUnderlyingType())) {
                        return intersections.get(type.getUnderlyingType());
                    }
                    break;
                case TYPEVAR:
                    TypeVariable key =
                            (TypeVariable)
                                    TypeAnnotationUtils.unannotatedType(type.getUnderlyingType());
                    if (typevars.containsKey(key)) {
                        return typevars.get(key);
                    }
                    break;
                default:
                    // do nothing
            }
            visit(type);
            return type;
        }

        // ----------------------------------------------------------------------------------------
        //

        /**
         * Initialize {@code typeVar}'s upper bound.
         *
         * @param typeVar type variable whose upper bound is initialized
         */
        public void initializeUpperBound(AnnotatedTypeVariable typeVar) {
            AnnotatedTypeMirror upperBound = createAndSetUpperBound(typeVar);

            TypePathNode pathNode = new UpperBoundNode();
            currentStructure.addPathNode(pathNode);
            visit(upperBound);
            currentStructure.removePathNode(pathNode);
        }

        /**
         * Initialize {@code typeVar}'s lower bound.
         *
         * @param typeVar type variable whose lower bound is initialized
         */
        public void initializeLowerBound(AnnotatedTypeVariable typeVar) {
            AnnotatedTypeMirror lowerBound = createAndSetLowerBound(typeVar);

            TypePathNode pathNode = new LowerBoundNode();
            currentStructure.addPathNode(pathNode);
            visit(lowerBound);
            currentStructure.removePathNode(pathNode);
        }

        /**
         * Initialize {@code wildcard}'s super bound.
         *
         * @param wildcard wildcard whose super bound is initialized
         */
        public void initializeSuperBound(AnnotatedWildcardType wildcard) {
            AnnotatedTypeFactory typeFactory = wildcard.atypeFactory;

            WildcardType underlyingType = wildcard.getUnderlyingType();
            TypeMirror underlyingSuperBound = underlyingType.getSuperBound();
            if (underlyingSuperBound == null) {
                underlyingSuperBound =
                        TypesUtils.wildLowerBound(
                                underlyingType, wildcard.atypeFactory.processingEnv);
            }

            AnnotatedTypeMirror superBound =
                    AnnotatedTypeMirror.createType(underlyingSuperBound, typeFactory, false);
            wildcard.setSuperBound(superBound);

            this.wildcards.put(wildcard.getUnderlyingType(), wildcard);

            TypePathNode superNode = currentStructure.addPathNode(new SuperBoundNode());
            visit(superBound);
            currentStructure.removePathNode(superNode);
        }

        /**
         * Initialize {@code wildcard}'s extends bound.
         *
         * @param wildcard wildcard whose extends bound is initialized
         */
        public void initializeExtendsBound(AnnotatedWildcardType wildcard) {
            AnnotatedTypeFactory typeFactory = wildcard.atypeFactory;

            WildcardType javaWildcardType = wildcard.getUnderlyingType();
            TypeMirror javaExtendsBound;
            if (javaWildcardType.getExtendsBound() != null) {
                // If the wildcard type has an extends bound, use it.
                javaExtendsBound = javaWildcardType.getExtendsBound();
            } else if (wildcard.getTypeVariable() != null) {
                // Otherwise use the upper bound of the type variable associated with this wildcard.
                javaExtendsBound = wildcard.getTypeVariable().getUpperBound();
            } else {
                // Otherwise use the upper bound of the java wildcard.
                javaExtendsBound =
                        TypesUtils.wildUpperBound(
                                javaWildcardType, wildcard.atypeFactory.processingEnv);
            }

            if (wildcard.isUninferredTypeArgument()) {
                rawTypeWildcards.put(wildcard.getTypeVariable(), wildcard.getUnderlyingType());
            }

            AnnotatedTypeMirror extendsBound =
                    AnnotatedTypeMirror.createType(javaExtendsBound, typeFactory, false);
            wildcard.setExtendsBound(extendsBound);

            this.wildcards.put(wildcard.getUnderlyingType(), wildcard);

            TypePathNode extendsNode = currentStructure.addPathNode(new ExtendsBoundNode());
            visit(extendsBound);
            currentStructure.removePathNode(extendsNode);
        }

        /**
         * Initialize {@code declaredType}'s type arguments.
         *
         * @param declaredType declared type whose type arguments are initialized
         */
        private void initializeTypeArgs(AnnotatedDeclaredType declaredType) {
            DeclaredType actualType = (DeclaredType) declaredType.actualType;
            if (actualType.getTypeArguments().isEmpty() && !declaredType.wasRaw()) {
                return;
            }
            TypeElement typeElement =
                    (TypeElement) declaredType.atypeFactory.types.asElement(actualType);
            List<AnnotatedTypeMirror> typeArgs;
            if (declaredType.typeArgs == null) {
                typeArgs = new ArrayList<>();
                for (int i = 0; i < typeElement.getTypeParameters().size(); i++) {
                    TypeMirror javaTypeArg =
                            getJavaType(declaredType, typeElement.getTypeParameters(), i);
                    AnnotatedTypeMirror atmArg =
                            AnnotatedTypeMirror.createType(
                                    javaTypeArg, declaredType.atypeFactory, false);
                    typeArgs.add(atmArg);
                    if (atmArg.getKind() == TypeKind.WILDCARD && declaredType.wasRaw()) {
                        ((AnnotatedWildcardType) atmArg).setUninferredTypeArgument();
                    }
                }
            } else {
                typeArgs = declaredType.typeArgs;
            }

            List<AnnotatedTypeMirror> typeArgReplacements = new ArrayList<>(typeArgs.size());
            for (int i = 0; i < typeArgs.size(); i++) {
                AnnotatedTypeMirror typeArg = typeArgs.get(i);
                TypePathNode node = currentStructure.addPathNode(new TypeArgNode(i));
                if (typeArg.getKind() == TypeKind.WILDCARD) {
                    ((AnnotatedWildcardType) typeArg)
                            .setTypeVariable(typeElement.getTypeParameters().get(i));
                }
                typeArgReplacements.add(getOrVisit(typeArg));
                currentStructure.removePathNode(node);
            }

            declaredType.setTypeArguments(typeArgReplacements);
        }

        /**
         * Store the wildcards created as type arguments to raw types.
         *
         * <p>{@code class Foo<T extends Foo> {}} The upper bound of the wildcard in {@code Foo<?>}
         * is {@code Foo}. The type argument of {@code Foo} is initialized to {@code ? extends Foo}.
         * The type argument of {@code Foo} in {@code ? extends Foo} needs to be initialized to the
         * same type argument as the first {@code Foo} so that
         * BoundsInitializer.InitializerVisitor#getOrVisit will return the cached
         * AnnotatedWildcardType.
         */
        private final Map<TypeVariable, WildcardType> rawTypeWildcards = new HashMap<>();

        /**
         * Returns the underlying java type of the {@code i}-th type argument of {@code type}. If
         * {@code type} is raw, then a new wildcard is created or returned from {@code
         * rawTypeWildcards}.
         *
         * @param type declared type
         * @param parameters elements of the type parameters
         * @param i index of the type parameter
         * @return the underlying java type of the {@code i}-th type argument of {@code type}
         */
        private TypeMirror getJavaType(
                AnnotatedDeclaredType type,
                List<? extends TypeParameterElement> parameters,
                int i) {
            if (type.wasRaw()) {
                TypeVariable typeVariable = (TypeVariable) parameters.get(i).asType();
                if (rawTypeWildcards.containsKey(typeVariable)) {
                    return rawTypeWildcards.get(typeVariable);
                }
                WildcardType wildcard = getUpperBoundAsWildcard(typeVariable, type.atypeFactory);
                rawTypeWildcards.put(typeVariable, wildcard);
                return wildcard;
            } else {
                return type.getUnderlyingType().getTypeArguments().get(i);
            }
        }

        /**
         * Replace all type variables in type with the AnnotatedTypeMirrors created when
         * initializing it.
         *
         * @param type all type variables are replaced
         */
        public void resolveTypeVarReferences(AnnotatedTypeMirror type) {
            List<AnnotatedTypeVariable> annotatedTypeVars = new ArrayList<>();
            if (type.getKind() == TypeKind.TYPEVAR) {
                annotatedTypeVars.add((AnnotatedTypeVariable) type);
            }

            // Gather a list of all AnnotatedTypeVariables and all the replacements to perform.
            for (TypeVariableStructure typeVarStruct : typeVarToStructure.values()) {
                typeVarStruct.findAllReplacements(typeVarToStructure);
                annotatedTypeVars.addAll(typeVarStruct.getAnnotatedTypeVars());
            }

            // Do the replacements.
            for (AnnotatedTypeVariable atv : annotatedTypeVars) {
                TypeVariableStructure list = typeVarToStructure.get(atv.getUnderlyingType());
                list.replaceTypeVariablesInType(atv);
            }

            if (type.getKind() == TypeKind.WILDCARD) {
                // Do the "top level" replacements.
                AnnotatedWildcardType wildcard = (AnnotatedWildcardType) type;
                topLevelStructure.findAllReplacements(typeVarToStructure);
                for (AnnotatedTypeVariable typeVar : topLevelStructure.getAnnotatedTypeVars()) {
                    TypeVariableStructure list =
                            typeVarToStructure.get(typeVar.getUnderlyingType());
                    list.replaceTypeVariablesInType(typeVar);
                }
                topLevelStructure.replaceTypeVariablesInType(wildcard);
            }
        }
    }

    /**
     * Creates the upper bound type for {@code typeVar} and sets it.
     *
     * @param typeVar type variable
     * @return the newly created upper bound
     */
    private static AnnotatedTypeMirror createAndSetUpperBound(AnnotatedTypeVariable typeVar) {
        AnnotatedTypeMirror upperBound =
                AnnotatedTypeMirror.createType(
                        typeVar.getUnderlyingType().getUpperBound(), typeVar.atypeFactory, false);
        typeVar.setUpperBound(upperBound);
        return upperBound;
    }

    /**
     * Creates the lower bound type for {@code typeVar} and sets it. If the type variable does not
     * have a lower bound, then a null type is created.
     *
     * @param typeVar type variable
     * @return the newly created lower bound
     */
    private static AnnotatedTypeMirror createAndSetLowerBound(AnnotatedTypeVariable typeVar) {
        TypeMirror lb = typeVar.getUnderlyingType().getLowerBound();
        if (lb == null) {
            // Use bottom type to ensure there is a lower bound.
            Context context =
                    ((JavacProcessingEnvironment) typeVar.atypeFactory.processingEnv).getContext();
            Symtab syms = Symtab.instance(context);
            lb = syms.botType;
        }
        AnnotatedTypeMirror lowerBound =
                AnnotatedTypeMirror.createType(lb, typeVar.atypeFactory, false);
        typeVar.setLowerBound(lowerBound);
        return lowerBound;
    }

    /**
     * Contains all the type variables and the type path to reach them found when scanning a
     * particular type variable or wildcard. Then uses this information to replace the type
     * variables with AnnotatedTypeVariables.
     */
    private static class RecursiveTypeStructure {

        /** List of TypePath and TypeVariables that were found will traversing this type. */
        private final List<Pair<TypePath, TypeVariable>> typeVarsInType = new ArrayList<>();

        /** Current path used to mark the locations of TypeVariables. */
        private final TypePath currentPath = new TypePath();

        /**
         * Add a type variable found at the current path while visiting the type variable or
         * wildcard associated with this structure.
         *
         * @param typeVariable TypeVariable
         */
        public void addTypeVar(TypeVariable typeVariable) {
            typeVarsInType.add(Pair.of(this.currentPath.copy(), typeVariable));
        }

        /**
         * Add a node in the path.
         *
         * @param node node to add
         * @return {@code node}
         */
        public TypePathNode addPathNode(TypePathNode node) {
            currentPath.add(node);
            return node;
        }

        /**
         * Remove the last node in the path if it is {@code node}; otherwise, throw an exception.
         *
         * @param node last node in the path
         */
        public void removePathNode(TypePathNode node) {
            if (currentPath.getLeaf() != node) {
                throw new BugInCF(
                        "Cannot remove node: %s. It is not the last node. currentPath= %s",
                        node, currentPath);
            }
            currentPath.removeLeaf();
        }

        /**
         * For all type variables contained within the type variable or wildcard that this structure
         * represents, this a list of the replacement {@link AnnotatedTypeVariable} for the location
         * specified by the {@link TypePath}.
         */
        private List<Pair<TypePath, AnnotatedTypeVariable>> replacementList;

        /**
         * Find the AnnotatedTypeVariables that should replace the type variables found in this
         * type.
         *
         * @param typeVarToStructure a mapping from TypeVariable to TypeVariableStructure
         */
        public void findAllReplacements(
                Map<TypeVariable, TypeVariableStructure> typeVarToStructure) {
            this.annotatedTypeVariables = new ArrayList<>();
            this.replacementList = new ArrayList<>();
            for (Pair<TypePath, TypeVariable> pair : typeVarsInType) {
                TypeVariableStructure targetStructure = typeVarToStructure.get(pair.second);
                AnnotatedTypeVariable template =
                        targetStructure.annotatedTypeVar.deepCopy().asUse();
                annotatedTypeVariables.add(template);
                replacementList.add(Pair.of(pair.first, template));
            }
        }

        /** List of {@link AnnotatedTypeVariable}s found in this type. */
        private List<AnnotatedTypeVariable> annotatedTypeVariables;

        /**
         * A list of all AnnotatedTypeVariables found in this type. {@link
         * #findAllReplacements(Map)} must be called first.
         *
         * @return A list of all AnnotatedTypeVariables found in this type
         */
        public List<AnnotatedTypeVariable> getAnnotatedTypeVars() {
            if (annotatedTypeVariables == null) {
                throw new BugInCF("Call createReplacementList before calling this method.");
            }
            return annotatedTypeVariables;
        }

        /**
         * Replaces all type variables in {@code type} with their replacements. ({@link
         * #findAllReplacements(Map)} must be called first so that the replacements can be found.)
         *
         * @param type annotated type whose type variables are replaced
         */
        public void replaceTypeVariablesInType(AnnotatedTypeMirror type) {
            if (replacementList == null) {
                throw new BugInCF("Call createReplacementList before calling this method.");
            }
            for (Pair<TypePath, AnnotatedTypeVariable> entry : replacementList) {
                TypePath path = entry.first;
                AnnotatedTypeVariable replacement = entry.second;
                path.replaceTypeVariable(type, replacement);
            }
        }
    }

    /** A {@link RecursiveTypeStructure} for a type variable. */
    private static class TypeVariableStructure extends RecursiveTypeStructure {
        /** The type variable whose structure is being described. */
        public final TypeVariable typeVar;

        /**
         * The first annotated type variable that was encountered and traversed in order to describe
         * typeVar. It is expanded during visitation and it is later used as a template for other
         * uses of typeVar
         */
        public final AnnotatedTypeVariable annotatedTypeVar;

        /**
         * Creates an {@link TypeVariableStructure}
         *
         * @param annotatedTypeVar annotated type for the type variable whose structure is being
         *     described
         */
        public TypeVariableStructure(AnnotatedTypeVariable annotatedTypeVar) {
            this.typeVar = annotatedTypeVar.getUnderlyingType();
            this.annotatedTypeVar = annotatedTypeVar;
        }
    }

    /**
     * A list of {@link TypePathNode}s. Each node represents a "location" of a composite type. For
     * example, an {@link UpperBoundNode} represents the upper bound type of a type variable
     */
    @SuppressWarnings("serial")
    private static class TypePath extends ArrayList<TypePathNode> {

        @Override
        public String toString() {
            return PluginUtil.join(",", this);
        }

        /**
         * Create a copy of this path
         *
         * @return a copy of this path
         */
        public TypePath copy() {
            TypePath copy = new TypePath();
            for (TypePathNode node : this) {
                copy.add(node.copy());
            }
            return copy;
        }

        /**
         * Return the leaf node of this path.
         *
         * @return the leaf node or null if the path is empty
         */
        public TypePathNode getLeaf() {
            if (this.isEmpty()) {
                return null;
            }
            return this.get(size() - 1);
        }

        /** Remove the leaf node if one exists. */
        public void removeLeaf() {
            if (this.isEmpty()) {
                return;
            }
            this.remove(size() - 1);
        }

        /**
         * In {@code type}, replace the type at the location specified by this path with {@code
         * replacement}.
         *
         * @param type annotated type that is side-effected
         * @param replacement annotated type to add to {@code type}
         */
        public void replaceTypeVariable(
                AnnotatedTypeMirror type, AnnotatedTypeVariable replacement) {
            AnnotatedTypeMirror current = type;
            for (int i = 0; i < size() - 1; i++) {
                current = get(i).getType(current);
            }
            this.getLeaf().replaceType(current, replacement);
        }
    }

    /**
     * A {@link TypePathNode} represents a "location" of a composite type. For example, an {@link
     * UpperBoundNode} represents the upper bound type of a type variable.
     */
    private abstract static class TypePathNode {

        /** The {@link TypeKind} of the parent of this node. */
        public final TypeKind parentTypeKind;

        /**
         * Creates a {@link TypePathNode}.
         *
         * @param parentTypeKind kind of parent of this node
         */
        TypePathNode(TypeKind parentTypeKind) {
            this.parentTypeKind = parentTypeKind;
        }

        /**
         * Copy constructor
         *
         * @param template node to copy
         */
        TypePathNode(TypePathNode template) {
            this.parentTypeKind = template.parentTypeKind;
        }

        @Override
        public String toString() {
            return this.getClass().getSimpleName();
        }

        /**
         * Returns the annotated type at the location represented by this node in {@code type}.
         *
         * @param type parent type
         * @return the annotated type at the location represented by this node in {@code type}
         * @throws BugInCF if {@code type} does not have a type at this location
         */
        public final AnnotatedTypeMirror getType(AnnotatedTypeMirror type) {
            abortIfNotKind(parentTypeKind, null, type);
            return getTypeInternal(type);
        }

        /**
         * Internal implementation of {@link #getType(AnnotatedTypeMirror)}.
         *
         * @param parent type that is sideffected by this method
         * @return the annotated type at the location represented by this node in {@code type}
         */
        protected abstract AnnotatedTypeMirror getTypeInternal(AnnotatedTypeMirror parent);

        /**
         * Replaces the type at the location represented by this node in {@code parent} with {@code
         * replacement}.
         *
         * @param parent type that is sideffected by this method
         * @param replacement the replacement
         * @throws BugInCF if {@code type} does not have a type at this location
         */
        public final void replaceType(
                AnnotatedTypeMirror parent, AnnotatedTypeVariable replacement) {
            abortIfNotKind(parentTypeKind, replacement, parent);
            replaceTypeInternal(parent, replacement);
        }

        /**
         * Internal implementation of #replaceType.
         *
         * @param parent type that is sideffected by this method
         * @param replacement the replacement
         */
        protected abstract void replaceTypeInternal(
                AnnotatedTypeMirror parent, AnnotatedTypeVariable replacement);

        /**
         * Returns a copy of the node.
         *
         * @return a copy of this node
         */
        public abstract TypePathNode copy();

        /**
         * Throws a {@link BugInCF} if {@code parent} is {@code typeKind}.
         *
         * @param typeKind TypeKind
         * @param replacement for debugging
         * @param parent possible parent type of this node
         * @throws BugInCF if {@code parent} is {@code typeKind}
         */
        private void abortIfNotKind(
                TypeKind typeKind, AnnotatedTypeVariable replacement, AnnotatedTypeMirror parent) {
            if (parent.getKind() == typeKind) {
                return;
            }

            throw new BugInCF(
                    "Unexpected parent kind:%nparent= %s%nreplacements= %s%n expected= %s",
                    parent, replacement, typeKind);
        }
    }

    /** Represents an enclosing type of a declared type. */
    private static class EnclosingTypeNode extends TypePathNode {

        /** Create an enclosing node. */
        EnclosingTypeNode() {
            super(TypeKind.DECLARED);
        }

        @Override
        protected void replaceTypeInternal(
                AnnotatedTypeMirror parent, AnnotatedTypeVariable replacement) {
            // An enclosing type cannot be a type variable, so do nothing.
        }

        @Override
        public AnnotatedDeclaredType getTypeInternal(AnnotatedTypeMirror parent) {
            return ((AnnotatedDeclaredType) parent).getEnclosingType();
        }

        @Override
        public TypePathNode copy() {
            return new EnclosingTypeNode();
        }
    }

    /** Represents an extends bound of a wildcard. */
    private static class ExtendsBoundNode extends TypePathNode {
        /** Creates an ExtendsBoundNode. */
        ExtendsBoundNode() {
            super(TypeKind.WILDCARD);
        }

        @Override
        protected void replaceTypeInternal(
                AnnotatedTypeMirror parent, AnnotatedTypeVariable replacement) {
            ((AnnotatedWildcardType) parent).setExtendsBound(replacement);
        }

        @Override
        protected AnnotatedTypeMirror getTypeInternal(AnnotatedTypeMirror parent) {
            return ((AnnotatedWildcardType) parent).getExtendsBound();
        }

        @Override
        public TypePathNode copy() {
            return new ExtendsBoundNode();
        }
    }

    /** Represents a super bound of a wildcard. */
    private static class SuperBoundNode extends TypePathNode {
        /** Creates a SuperBoundNode. */
        SuperBoundNode() {
            super(TypeKind.WILDCARD);
        }

        @Override
        protected void replaceTypeInternal(
                AnnotatedTypeMirror parent, AnnotatedTypeVariable replacement) {
            ((AnnotatedWildcardType) parent).setSuperBound(replacement);
        }

        @Override
        protected AnnotatedTypeMirror getTypeInternal(AnnotatedTypeMirror parent) {
            return ((AnnotatedWildcardType) parent).getSuperBound();
        }

        @Override
        public TypePathNode copy() {
            return new SuperBoundNode();
        }
    }

    /** Represents an upper bound of a type variable. */
    private static class UpperBoundNode extends TypePathNode {

        /** Creates an UpperBoundNode. */
        UpperBoundNode() {
            super(TypeKind.TYPEVAR);
        }

        @Override
        protected void replaceTypeInternal(
                AnnotatedTypeMirror parent, AnnotatedTypeVariable replacement) {
            ((AnnotatedTypeVariable) parent).setUpperBound(replacement);
        }

        @Override
        protected AnnotatedTypeMirror getTypeInternal(AnnotatedTypeMirror parent) {
            AnnotatedTypeVariable parentAtv = (AnnotatedTypeVariable) parent;
            if (parentAtv.getUpperBoundField() != null) {
                return parentAtv.getUpperBoundField();
            }
            return createAndSetUpperBound((AnnotatedTypeVariable) parent);
        }

        @Override
        public TypePathNode copy() {
            return new UpperBoundNode();
        }
    }

    /** Represents a lower bound of a type variable. */
    private static class LowerBoundNode extends TypePathNode {

        /** Creates a LowerBoundNode */
        LowerBoundNode() {
            super(TypeKind.TYPEVAR);
        }

        @Override
        protected void replaceTypeInternal(
                AnnotatedTypeMirror parent, AnnotatedTypeVariable replacement) {
            ((AnnotatedTypeVariable) parent).setLowerBound(replacement);
        }

        @Override
        protected AnnotatedTypeMirror getTypeInternal(AnnotatedTypeMirror parent) {

            AnnotatedTypeVariable parentAtv = (AnnotatedTypeVariable) parent;
            if (parentAtv.getLowerBoundField() != null) {
                return parentAtv.getLowerBoundField();
            }
            // else // TODO: I think this should never happen at this point, throw exception
            return createAndSetLowerBound((AnnotatedTypeVariable) parent);
        }

        @Override
        public TypePathNode copy() {
            return new LowerBoundNode();
        }
    }

    /** Represents a component type of an array type. */
    private static class ArrayComponentNode extends TypePathNode {

        /** Create ArrayComponentNode. */
        ArrayComponentNode() {
            super(TypeKind.ARRAY);
        }

        @Override
        protected void replaceTypeInternal(
                AnnotatedTypeMirror parent, AnnotatedTypeVariable replacement) {
            ((AnnotatedArrayType) parent).setComponentType(replacement);
        }

        @Override
        protected AnnotatedTypeMirror getTypeInternal(AnnotatedTypeMirror parent) {
            return ((AnnotatedArrayType) parent).getComponentType();
        }

        @Override
        public TypePathNode copy() {
            return new ArrayComponentNode();
        }
    }

    /** A bound type of an intersection type. */
    private static class IntersectionBoundNode extends TypePathNode {

        /** The index of the particular bound type of an intersection type this node represents. */
        public final int superIndex;

        /**
         * Creates an IntersectionBoundNode
         *
         * @param superIndex the index of the particular bound type of an intersection type this
         *     node represents.
         */
        IntersectionBoundNode(int superIndex) {
            super(TypeKind.INTERSECTION);
            this.superIndex = superIndex;
        }

        /**
         * Copy constructor.
         *
         * @param template node to copy
         */
        IntersectionBoundNode(IntersectionBoundNode template) {
            super(template);
            superIndex = template.superIndex;
        }

        @Override
        public String toString() {
            return super.toString() + "( superIndex=" + superIndex + " )";
        }

        @Override
        protected void replaceTypeInternal(
                AnnotatedTypeMirror parent, AnnotatedTypeVariable replacement) {
            throw new BugInCF(
                    "Type variables cannot be intersection bounds.%nparent=%s%nreplacement=%s",
                    parent, replacement);
        }

        @Override
        protected AnnotatedTypeMirror getTypeInternal(AnnotatedTypeMirror parent) {
            AnnotatedIntersectionType isect = (AnnotatedIntersectionType) parent;
            if (parent.directSuperTypes().size() <= superIndex) {
                throw new BugInCF("Invalid superIndex %d: parent=%s", superIndex, parent);
            }

            return isect.directSuperTypes().get(superIndex);
        }

        @Override
        public TypePathNode copy() {
            return new IntersectionBoundNode(this);
        }
    }

    /** Represents an alternative type of a union node. */
    private static class AlternativeTypeNode extends TypePathNode {

        /**
         * The index of the particular alternative type of the union node that this node represents.
         */
        public final int altIndex;

        /**
         * Creates a AlternativeTypeNode.
         *
         * @param altIndex the index of the particular alternative type of the union node that this
         *     node represents
         */
        AlternativeTypeNode(int altIndex) {
            super(TypeKind.UNION);
            this.altIndex = altIndex;
        }

        /**
         * Copy constructor.
         *
         * @param template node to copy
         */
        AlternativeTypeNode(AlternativeTypeNode template) {
            super(template);
            altIndex = template.altIndex;
        }

        @Override
        public String toString() {
            return super.toString() + "( altIndex=" + altIndex + " )";
        }

        @Override
        protected void replaceTypeInternal(
                AnnotatedTypeMirror parent, AnnotatedTypeVariable replacement) {
            throw new BugInCF(
                    "Union types cannot be intersection bounds.%nparent=%s%nreplacement=%s",
                    parent, replacement);
        }

        @Override
        protected AnnotatedTypeMirror getTypeInternal(AnnotatedTypeMirror parent) {
            AnnotatedUnionType isect = (AnnotatedUnionType) parent;
            if (parent.directSuperTypes().size() <= altIndex) {
                throw new BugInCF("Invalid altIndex( %s ):%nparent=%s", altIndex, parent);
            }

            return isect.directSuperTypes().get(altIndex);
        }

        @Override
        public TypePathNode copy() {
            return new AlternativeTypeNode(this);
        }
    }

    /** Represents a type argument of a declared type. */
    private static class TypeArgNode extends TypePathNode {

        /** The index of the type argument that this node represents. */
        public final int argIndex;

        /**
         * Creates a TypeArgumentNode.
         *
         * @param argIndex index of the type argument that this node represents.
         */
        TypeArgNode(int argIndex) {
            super(TypeKind.DECLARED);
            this.argIndex = argIndex;
        }

        /**
         * Copy constructor.
         *
         * @param template node to copy
         */
        TypeArgNode(TypeArgNode template) {
            super(template);
            this.argIndex = template.argIndex;
        }

        @Override
        public String toString() {
            return super.toString() + "( argIndex=" + argIndex + " )";
        }

        @Override
        protected void replaceTypeInternal(
                AnnotatedTypeMirror parent, AnnotatedTypeVariable replacement) {
            AnnotatedDeclaredType parentAdt = (AnnotatedDeclaredType) parent;
            List<AnnotatedTypeMirror> typeArgs = new ArrayList<>(parentAdt.getTypeArguments());
            if (argIndex >= typeArgs.size()) {
                throw new BugInCF(
                        PluginUtil.joinLines(
                                "Invalid type arg index.",
                                "parent=" + parent,
                                "replacement=" + replacement,
                                "argIndex=" + argIndex));
            }
            typeArgs.add(argIndex, replacement);
            typeArgs.remove(argIndex + 1);
            parentAdt.setTypeArguments(typeArgs);
        }

        @Override
        protected AnnotatedTypeMirror getTypeInternal(AnnotatedTypeMirror parent) {
            AnnotatedDeclaredType parentAdt = (AnnotatedDeclaredType) parent;

            List<AnnotatedTypeMirror> typeArgs = parentAdt.getTypeArguments();
            if (argIndex >= typeArgs.size()) {
                throw new BugInCF(
                        PluginUtil.joinLines(
                                "Invalid type arg index.",
                                "parent=" + parent,
                                "argIndex=" + argIndex));
            }

            return typeArgs.get(argIndex);
        }

        @Override
        public TypePathNode copy() {
            return new TypeArgNode(this);
        }
    }
}
