package org.checkerframework.framework.type;

import java.util.IdentityHashMap;
import java.util.Map;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable;

/** Duplicates annotated types and replaces components according to a replacement map. */
public class AnnotatedTypeReplacer {

    /**
     * Return a copy of type after making the specified replacements.
     *
     * @param type the type that will be copied with replaced components
     * @param replacementMap a mapping of {@literal referenceToReplace &rArr;
     *     referenceOfReplacement}
     * @return a duplicate of type in which every reference that was a key in replacementMap has
     *     been replaced by its corresponding value
     */
    public static AnnotatedTypeMirror replace(
            AnnotatedTypeMirror type,
            Map<? extends AnnotatedTypeMirror, ? extends AnnotatedTypeMirror> replacementMap) {
        return new Visitor(replacementMap).visit(type);
    }

    /**
     * AnnotatedTypeCopier maintains a mapping of {@literal typeVisited &rArr; copyOfTypeVisited}
     * When a reference, typeVisited, is encountered again, it will use the recorded reference,
     * copyOfTypeVisited, instead of generating a new copy of typeVisited. Visitor pre-populates
     * this mapping so that references are replaced not by their copies but by those in the
     * replacementMap provided in the constructor.
     *
     * <p>All types NOT in the replacement map are duplicated as per AnnotatedTypeCopier.visit
     */
    protected static class Visitor extends AnnotatedTypeCopier {

        private final IdentityHashMap<? extends AnnotatedTypeMirror, ? extends AnnotatedTypeMirror>
                originalMappings;

        public Visitor(
                final Map<? extends AnnotatedTypeMirror, ? extends AnnotatedTypeMirror> mappings) {
            originalMappings = new IdentityHashMap<>(mappings);
        }

        @Override
        public AnnotatedTypeMirror visit(AnnotatedTypeMirror type) {
            return type.accept(this, new IdentityHashMap<>(originalMappings));
        }

        @Override
        public AnnotatedTypeMirror visitTypeVariable(
                AnnotatedTypeVariable original,
                IdentityHashMap<AnnotatedTypeMirror, AnnotatedTypeMirror> originalToCopy) {
            // AnnotatedTypeCopier will visit the type parameters of a method and copy them.
            // Without this flag, any mappings in originalToCopy would replace the type parameters.
            // However, we do not replace the type parameters in an AnnotatedExecutableType.  Also,
            // AnnotatedExecutableType.typeVarTypes is of type List<AnnotatedTypeVariable> so if the
            // mapping contained a type parameter -> (Non-type variable AnnotatedTypeMirror) then a
            // runtime exception would occur.
            if (visitingExecutableTypeParam) {
                visitingExecutableTypeParam = false;
                final AnnotatedTypeVariable copy =
                        (AnnotatedTypeVariable)
                                AnnotatedTypeMirror.createType(
                                        original.getUnderlyingType(),
                                        original.atypeFactory,
                                        original.isDeclaration());
                maybeCopyPrimaryAnnotations(original, copy);
                originalToCopy.put(original, copy);

                if (original.getUpperBoundField() != null) {
                    copy.setUpperBound(visit(original.getUpperBoundField(), originalToCopy));
                }

                if (original.getLowerBoundField() != null) {
                    copy.setLowerBound(visit(original.getLowerBoundField(), originalToCopy));
                }
                return copy;
            }

            return super.visitTypeVariable(original, originalToCopy);
        }
    }
}
