package org.checkerframework.framework.type;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.type.TypeKind;
import javax.lang.model.util.Types;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedIntersectionType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedPrimitiveType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedWildcardType;
import org.checkerframework.framework.type.visitor.AbstractAtmComboVisitor;
import org.checkerframework.framework.util.AnnotatedTypes;
import org.checkerframework.framework.util.AtmCombo;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.PluginUtil;
import org.checkerframework.javacutil.TypesUtils;

/**
 * A visitor used to compare two type mirrors for "structural" equality. Structural equality implies
 * that, for two objects, all fields are also structurally equal and for primitives their values are
 * equal. One reason this class is necessary is that at the moment we compare wildcards and type
 * variables for "equality". This occurs because we do not employ capture conversion.
 *
 * <p>See also DefaultTypeHierarchy, and SubtypeVisitHistory
 */
public class StructuralEqualityComparer extends AbstractAtmComboVisitor<Boolean, Void> {
    /** History saving the result of previous comparisons. */
    protected final StructuralEqualityVisitHistory visitHistory;

    // See org.checkerframework.framework.type.DefaultTypeHierarchy.currentTop
    private AnnotationMirror currentTop = null;

    /**
     * Create a StructuralEqualityComparer.
     *
     * @param typeargVisitHistory history saving the result of previous comparisons
     */
    public StructuralEqualityComparer(StructuralEqualityVisitHistory typeargVisitHistory) {
        this.visitHistory = typeargVisitHistory;
    }

    @Override
    protected Boolean defaultAction(AnnotatedTypeMirror type1, AnnotatedTypeMirror type2, Void p) {
        if (type1.atypeFactory.ignoreUninferredTypeArguments) {
            if (type1.getKind() == TypeKind.WILDCARD
                    && ((AnnotatedWildcardType) type1).isUninferredTypeArgument()) {
                return true;
            }

            if (type2.getKind() == TypeKind.WILDCARD
                    && ((AnnotatedWildcardType) type2).isUninferredTypeArgument()) {
                return true;
            }
        }
        if (type1.getKind() == TypeKind.TYPEVAR || type2.getKind() == TypeKind.TYPEVAR) {
            // TODO: Handle any remaining typevar combinations correctly.
            return true;
        }
        if (type1.getKind() == TypeKind.NULL || type2.getKind() == TypeKind.NULL) {
            // If one of the types is the NULL type, compare main qualifiers only.
            return arePrimeAnnosEqual(type1, type2);
        }
        return super.defaultAction(type1, type2, p);
    }

    /**
     * Called for every combination that isn't specifically handled.
     *
     * @return error message explaining the two types' classes are not the same
     */
    @Override
    protected String defaultErrorMessage(
            AnnotatedTypeMirror type1, AnnotatedTypeMirror type2, Void p) {
        return "AnnotatedTypeMirrors aren't structurally equal.\n"
                + "  type1 = "
                + type1.getClass().getSimpleName()
                + "( "
                + type1
                + " )\n"
                + "  type2 = "
                + type2.getClass().getSimpleName()
                + "( "
                + type2
                + " )\n"
                + "  visitHistory = "
                + visitHistory;
    }

    /**
     * Returns true if type1 and type2 are structurally equivalent. With one exception,
     * type1.getClass().equals(type2.getClass()) must be true. However, because the Checker
     * Framework sometimes "infers" Typevars to be Wildcards, we allow the combination
     * Wildcard,Typevar. In this case, the two types are "equal" if their bounds are.
     *
     * @return true if type1 and type2 are equal
     */
    private boolean areEqual(final AnnotatedTypeMirror type1, final AnnotatedTypeMirror type2) {
        assert currentTop != null;
        if (type1 == type2) {
            return true;
        }
        if (type1 == null || type2 == null) {
            return false;
        }
        return AtmCombo.accept(type1, type2, null, this);
    }

    public boolean areEqualInHierarchy(
            final AnnotatedTypeMirror type1,
            final AnnotatedTypeMirror type2,
            final AnnotationMirror top) {
        assert top != null;
        boolean areEqual;
        AnnotationMirror prevTop = currentTop;
        currentTop = top;
        try {
            areEqual = areEqual(type1, type2);
        } finally {
            currentTop = prevTop;
        }

        return areEqual;
    }

    /** Return true if type1 and type2 have the same set of annotations. */
    protected boolean arePrimeAnnosEqual(
            final AnnotatedTypeMirror type1, final AnnotatedTypeMirror type2) {
        if (currentTop != null) {
            return AnnotationUtils.areSame(
                    type1.getAnnotationInHierarchy(currentTop),
                    type2.getAnnotationInHierarchy(currentTop));
        } else {
            throw new BugInCF("currentTop null");
        }
    }

    /**
     * Compare each type in types1 and types2 pairwise and return true if they are all equal. This
     * method throws an exceptions if types1.size() != types2.size()
     *
     * @return true if for each pair (t1 = types1.get(i); t2 = types2.get(i)), areEqual(t1,t2)
     */
    protected boolean areAllEqual(
            final Collection<? extends AnnotatedTypeMirror> types1,
            final Collection<? extends AnnotatedTypeMirror> types2) {
        if (types1.size() != types2.size()) {
            throw new BugInCF(
                    "Mismatching collection sizes:\n    types 1: "
                            + PluginUtil.join("; ", types1)
                            + " ("
                            + types1.size()
                            + ")\n    types 2: "
                            + PluginUtil.join("; ", types2)
                            + " ("
                            + types2.size()
                            + ")");
        }

        final Iterator<? extends AnnotatedTypeMirror> types1Iter = types1.iterator();
        final Iterator<? extends AnnotatedTypeMirror> types2Iter = types2.iterator();
        while (types1Iter.hasNext()) {
            final AnnotatedTypeMirror type1 = types1Iter.next();
            final AnnotatedTypeMirror type2 = types2Iter.next();
            if (!checkOrAreEqual(type1, type2)) {
                return false;
            }
        }

        return true;
    }

    /**
     * First check visitHistory to see if type1 and type2 have been compared once already. If so
     * return true; otherwise compare them and add them to visitHistory
     */
    protected boolean checkOrAreEqual(
            final AnnotatedTypeMirror type1, final AnnotatedTypeMirror type2) {
        Boolean pastResult = visitHistory.result(type1, type2, currentTop);
        if (pastResult != null) {
            return pastResult;
        }

        final Boolean result = areEqual(type1, type2);
        visitHistory.add(type1, type2, currentTop, result);
        return result;
    }

    /**
     * Two arrays are equal if:
     *
     * <ol>
     *   <li>Their sets of primary annotations are equal, and
     *   <li>Their component types are equal
     * </ol>
     */
    @Override
    public Boolean visitArray_Array(
            final AnnotatedArrayType type1, final AnnotatedArrayType type2, final Void p) {
        if (!arePrimeAnnosEqual(type1, type2)) {
            return false;
        }

        return areEqual(type1.getComponentType(), type2.getComponentType());
    }

    /**
     * Two declared types are equal if:
     *
     * <ol>
     *   <li>The types are of the same class/interfaces
     *   <li>Their sets of primary annotations are equal
     *   <li>Their sets of type arguments are equal or one type is raw
     * </ol>
     */
    @Override
    public Boolean visitDeclared_Declared(
            final AnnotatedDeclaredType type1, final AnnotatedDeclaredType type2, final Void p) {
        Boolean pastResult = visitHistory.result(type1, type2, currentTop);
        if (pastResult != null) {
            return pastResult;
        }

        // TODO: same class/interface is not enforced. Why?

        if (!arePrimeAnnosEqual(type1, type2)) {
            return false;
        }

        // Prevent infinite recursion e.g. in Issue1587b
        visitHistory.add(type1, type2, currentTop, true);

        boolean result = visitTypeArgs(type1, type2);
        visitHistory.add(type1, type2, currentTop, result);
        return result;
    }

    /**
     * A helper class for visitDeclared_Declared. There are subtypes of DefaultTypeHierarchy that
     * need to customize the handling of type arguments. This method provides a convenient extension
     * point.
     */
    protected boolean visitTypeArgs(
            final AnnotatedDeclaredType type1, final AnnotatedDeclaredType type2) {

        // TODO: ANYTHING WITH RAW TYPES? SHOULD WE HANDLE THEM LIKE DefaultTypeHierarchy, i.e. use
        // ignoreRawTypes
        final List<? extends AnnotatedTypeMirror> type1Args = type1.getTypeArguments();
        final List<? extends AnnotatedTypeMirror> type2Args = type2.getTypeArguments();

        if (type1Args.isEmpty() && type2Args.isEmpty()) {
            return true;
        }

        if (type1Args.size() == type2Args.size()) {
            return areAllEqual(type1Args, type2Args);
        } else {
            return true;
            /* TODO! This should be an error. See framework/tests/all-systems/InitializationVisitor.java
             * for a failure.
            throw new BugInCF(
                    "Mismatching type argument sizes:\n    type 1: "
                            + type1
                            + " ("
                            + type1Args.size()
                            + ")\n    type 2: "
                            + type2
                            + " ("
                            + type2Args.size()
                            + ")");
            return false;
            */
        }
    }

    /**
     * TODO: SHOULD PRIMARY ANNOTATIONS OVERRIDE INDIVIDUAL BOUND ANNOTATIONS? IF SO THEN WE SHOULD
     * REMOVE THE arePrimeAnnosEqual AND FIX AnnotatedIntersectionType.
     *
     * <p>Two intersection types are equal if:
     *
     * <ul>
     *   <li>Their sets of primary annotations are equal
     *   <li>Their sets of bounds (the types being intersected) are equal
     * </ul>
     */
    @Override
    public Boolean visitIntersection_Intersection(
            final AnnotatedIntersectionType type1,
            final AnnotatedIntersectionType type2,
            final Void p) {
        if (!arePrimeAnnosEqual(type1, type2)) {
            return false;
        }

        Boolean result = areAllEqual(type1.directSuperTypes(), type2.directSuperTypes());
        visitHistory.add(type1, type2, currentTop, result);
        return result;
    }

    /**
     * Two primitive types are equal if:
     *
     * <ul>
     *   <li>Their sets of primary annotations are equal
     * </ul>
     */
    @Override
    public Boolean visitPrimitive_Primitive(
            final AnnotatedPrimitiveType type1, final AnnotatedPrimitiveType type2, final Void p) {
        return arePrimeAnnosEqual(type1, type2);
    }

    /**
     * Two type variables are equal if:
     *
     * <ul>
     *   <li>Their bounds are equal
     * </ul>
     *
     * Note: Primary annotations will be taken into account when the bounds are retrieved
     */
    @Override
    public Boolean visitTypevar_Typevar(
            final AnnotatedTypeVariable type1, final AnnotatedTypeVariable type2, final Void p) {
        Boolean pastResult = visitHistory.result(type1, type2, currentTop);
        if (pastResult != null) {
            return pastResult;
        }

        // TODO: Remove this code when capture conversion is implemented
        if (TypesUtils.isCaptured(type1.getUnderlyingType())
                || TypesUtils.isCaptured(type2.getUnderlyingType())) {
            if (!boundsMatch(type1, type2)) {
                Boolean result =
                        subtypeAndCompare(type1.getUpperBound(), type2.getUpperBound())
                                && subtypeAndCompare(type1.getLowerBound(), type2.getLowerBound());
                visitHistory.add(type1, type2, currentTop, result);
                return result;
            }
        }

        Boolean result =
                areEqual(type1.getUpperBound(), type2.getUpperBound())
                        && areEqual(type1.getLowerBound(), type2.getLowerBound());
        visitHistory.add(type1, type2, currentTop, result);
        return result;
    }

    /**
     * A temporary solution until we handle CaptureConversion, subtypeAndCompare handles cases in
     * which we encounter a captured type being compared against a non-captured type. The captured
     * type may have type arguments that are subtypes of the other type it is being compared to. In
     * these cases, we will convert the bounds via this method to the other type and then continue
     * on with the equality comparison. If neither of the type args can be converted to the other
     * than we just compare the effective annotations on the two types for equality.
     */
    boolean subtypeAndCompare(final AnnotatedTypeMirror type1, final AnnotatedTypeMirror type2) {
        final Types types = type1.atypeFactory.types;
        final AnnotatedTypeMirror t1;
        final AnnotatedTypeMirror t2;

        if (type1.getKind() == TypeKind.NULL && type2.getKind() == TypeKind.NULL) {
            return areEqual(type1, type2);
        }
        if (type1.getKind() == TypeKind.NULL || type2.getKind() == TypeKind.NULL) {
            t1 = type1;
            t2 = type2;
        } else if (types.isSubtype(type2.getUnderlyingType(), type1.getUnderlyingType())) {
            t1 = type1;
            t2 = AnnotatedTypes.asSuper(type1.atypeFactory, type2, type1);

        } else if (types.isSubtype(type1.getUnderlyingType(), type2.getUnderlyingType())) {
            t1 = AnnotatedTypes.asSuper(type1.atypeFactory, type1, type2);
            t2 = type2;

        } else {
            t1 = null;
            t2 = null;
        }

        if (t1 == null || t2 == null) {
            final QualifierHierarchy qualifierHierarchy =
                    type1.atypeFactory.getQualifierHierarchy();
            if (currentTop != null) {
                return AnnotationUtils.areSame(
                        AnnotatedTypes.findEffectiveAnnotationInHierarchy(
                                qualifierHierarchy, type1, currentTop),
                        AnnotatedTypes.findEffectiveAnnotationInHierarchy(
                                qualifierHierarchy, type2, currentTop));
            } else {
                throw new BugInCF("currentTop null");
            }
        }

        return areEqual(t1, t2);
    }

    /** @return true if the underlying types of the bounds for type1 and type2 are equal */
    public boolean boundsMatch(
            final AnnotatedTypeVariable type1, final AnnotatedTypeVariable type2) {
        final Types types = type1.atypeFactory.types;

        return types.isSameType(
                        type1.getUpperBound().getUnderlyingType(),
                        type2.getUpperBound().getUnderlyingType())
                && types.isSameType(
                        type1.getLowerBound().getUnderlyingType(),
                        type2.getLowerBound().getUnderlyingType());
    }

    /**
     * Two wildcards are equal if:
     *
     * <ul>
     *   <li>Their bounds are equal
     * </ul>
     *
     * Note: Primary annotations will be taken into account when the bounds are retrieved
     *
     * <p>TODO: IDENTIFY TESTS THAT LEAD TO RECURSIVE BOUNDED WILDCARDS, PERHAPS THE RIGHT THING IS
     * TO MOVE THE CODE THAT IDENTIFIES REFERENCES TO THE SAME WILDCARD TYPE HERE. WOULD WE EVER
     * WANT TO HAVE A REFERENCE TO THE SAME WILDCARD WITH DIFFERENT ANNOTATIONS?
     */
    @Override
    public Boolean visitWildcard_Wildcard(
            final AnnotatedWildcardType type1, final AnnotatedWildcardType type2, final Void p) {
        Boolean pastResult = visitHistory.result(type1, type2, currentTop);
        if (pastResult != null) {
            return pastResult;
        }

        if (type1.atypeFactory.ignoreUninferredTypeArguments
                && (type1.isUninferredTypeArgument() || type2.isUninferredTypeArgument())) {
            return true;
        }

        Boolean result =
                areEqual(type1.getExtendsBound(), type2.getExtendsBound())
                        && areEqual(type1.getSuperBound(), type2.getSuperBound());
        visitHistory.add(type1, type2, currentTop, result);
        return result;
    }

    // since we don't do a boxing conversion between primitive and declared types in some cases
    // we must compare primitives with their boxed counterparts
    @Override
    public Boolean visitDeclared_Primitive(
            AnnotatedDeclaredType type1, AnnotatedPrimitiveType type2, Void p) {
        if (!TypesUtils.isBoxOf(type1.getUnderlyingType(), type2.getUnderlyingType())) {
            defaultErrorMessage(type1, type2, p);
        }

        return arePrimeAnnosEqual(type1, type2);
    }

    @Override
    public Boolean visitPrimitive_Declared(
            AnnotatedPrimitiveType type1, AnnotatedDeclaredType type2, Void p) {
        if (!TypesUtils.isBoxOf(type2.getUnderlyingType(), type1.getUnderlyingType())) {
            defaultErrorMessage(type1, type2, p);
        }

        return arePrimeAnnosEqual(type1, type2);
    }

    // The following methods are because we use WILDCARDS instead of TYPEVARS for capture converted
    // wildcards.
    // TODO: REMOVE THE METHOD BELOW WHEN CAPTURE CONVERSION IS IMPLEMENTED
    /**
     * Since the Checker Framework doesn't engage in capture conversion, and since sometimes type
     * variables are "inferred" to be wildcards, this method allows the comparison of a wildcard to
     * a type variable even though they should never truly be equal.
     *
     * <p>A wildcard is equal to a type variable if:
     *
     * <ul>
     *   <li>The wildcard's bounds are equal to the type variable's bounds
     * </ul>
     */
    @Override
    public Boolean visitWildcard_Typevar(
            final AnnotatedWildcardType type1, final AnnotatedTypeVariable type2, final Void p) {
        Boolean pastResult = visitHistory.result(type1, type2, currentTop);
        if (pastResult != null) {
            return pastResult;
        }

        if (type1.atypeFactory.ignoreUninferredTypeArguments && type1.isUninferredTypeArgument()) {
            return true;
        }

        Boolean result =
                areEqual(type1.getExtendsBound(), type2.getUpperBound())
                        && areEqual(type1.getSuperBound(), type2.getLowerBound());

        visitHistory.add(type1, type2, currentTop, result);
        return result;
    }

    @Override
    public Boolean visitWildcard_Declared(
            AnnotatedWildcardType type1, AnnotatedDeclaredType type2, Void p) {
        if (type1.atypeFactory.ignoreUninferredTypeArguments && type1.isUninferredTypeArgument()) {
            return true;
        }
        // TODO: add proper checks
        return arePrimeAnnosEqual(type1.getExtendsBound(), type2);
    }

    @Override
    public Boolean visitDeclared_Wildcard(
            AnnotatedDeclaredType type1, AnnotatedWildcardType type2, Void p) {
        if (type2.atypeFactory.ignoreUninferredTypeArguments && type2.isUninferredTypeArgument()) {
            return true;
        }
        final QualifierHierarchy qualifierHierarchy = type1.atypeFactory.getQualifierHierarchy();

        // TODO: add proper checks
        // TODO: compare whole types instead of just main modifiers
        AnnotationMirror q1 =
                AnnotatedTypes.findEffectiveAnnotationInHierarchy(
                        qualifierHierarchy, type1, currentTop);
        AnnotationMirror q2 =
                AnnotatedTypes.findEffectiveAnnotationInHierarchy(
                        qualifierHierarchy, type2, currentTop);

        Boolean result = qualifierHierarchy.isSubtype(q1, q2);
        visitHistory.add(type1, type2, currentTop, result);
        return result;
    }
}
