package org.checkerframework.framework.util.typeinference.constraint;

import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.lang.model.type.TypeVariable;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.util.typeinference.TypeArgInferenceUtil;

/**
 * AFConstraint represent the initial constraints used to infer type arguments for method
 * invocations and new class invocations. These constraints are simplified then converted to
 * TUConstraints during type argument inference.
 *
 * <p>Subclasses of AFConstraint represent the following types of constraints found in
 * (https://docs.oracle.com/javase/specs/jls/se11/html/jls-15.html#jls-15.12.2.7)
 *
 * <p>A 《 F and F 》 A both imply that A is convertible to F. F 《 A and A 》 F both imply that F is
 * convertible to A (this may happen due to wildcard/typevar bounds and recursive types) A = F
 * implies that A is exactly F
 *
 * <p>In the Checker Framework a type, A will be convertible to another type F, if
 * AnnotatedTypes.asSuper will return a non-null value when A is passed as a subtype and F the
 * supertype to the method.
 *
 * <p>In Java type A will be convertible to another type F if there exists a conversion
 * context/method that transforms the one type into the other.
 *
 * <p>A 《 F and F 》 A are represented by class A2F F 《 A and A 》 F are represented by class F2A F =
 * A is represented by class FIsA
 */
public abstract class AFConstraint {
    /** The argument type. */
    public final AnnotatedTypeMirror argument;
    /** The formal parameter type. */
    public final AnnotatedTypeMirror formalParameter;

    /** Create a constraint for type arguments for a methodd invocation or new class invocation. */
    public AFConstraint(
            final AnnotatedTypeMirror argument, final AnnotatedTypeMirror formalParameter) {
        this.argument = argument;
        this.formalParameter = formalParameter;
        TypeArgInferenceUtil.checkForUninferredTypes(argument);
    }

    /**
     * @param targets the type parameters whose arguments we are trying to solve for
     * @return true if this constraint can't be broken up into other constraints or further
     *     simplified In general, if either argument or formal parameter is a use of the type
     *     parameters we are inferring over then the constraint cannot be reduced further
     */
    public boolean isIrreducible(final Set<TypeVariable> targets) {
        return TypeArgInferenceUtil.isATarget(argument, targets)
                || TypeArgInferenceUtil.isATarget(formalParameter, targets);
    }

    @Override
    public boolean equals(@Nullable Object thatObject) {
        if (this == thatObject) {
            return true;
        } // else

        if (thatObject == null || this.getClass() != thatObject.getClass()) {
            return false;
        }

        final AFConstraint that = (AFConstraint) thatObject;

        return this.argument.equals(that.argument)
                && this.formalParameter.equals(that.formalParameter);
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.getClass(), formalParameter, argument);
    }

    /**
     * Once AFConstraints are irreducible it can be converted to a TU constraint, constraints
     * between individual type parameters for which we are inferring an argument (T) and Java types
     * (U).
     *
     * @return a TUConstraint that represents this AFConstraint
     */
    public abstract TUConstraint toTUConstraint();

    /**
     * Given a partial solution to our type argument inference, replace any uses of type parameters
     * that have been solved with their arguments.
     *
     * <p>That is: Let S be a partial solution to our inference (i.e. we have inferred type
     * arguments for some types) Let S be a map {@code (T0 &rArr; A0, T1 &rArr; A1, ..., TN &rArr;
     * AN)} where Ti is a type parameter and Ai is its solved argument. For all uses of Ti in this
     * constraint, replace them with Ai.
     *
     * <p>For the mapping {@code (T0 &rArr; A0)}, the following constraint: {@code ArrayList<T0> <<
     * List<T1>}
     *
     * <p>Becomes: {@code ArrayList<A0> << List<T1>}
     *
     * <p>A constraint: {@code T0 << T1}
     *
     * <p>Becomes: {@code A0 << T1}
     *
     * @param substitutions a mapping of target type parameter to the type argument to
     * @return a new constraint that contains no use of the keys in substitutions
     */
    public AFConstraint substitute(final Map<TypeVariable, AnnotatedTypeMirror> substitutions) {
        final AnnotatedTypeMirror newArgument =
                TypeArgInferenceUtil.substitute(substitutions, argument);
        final AnnotatedTypeMirror newFormalParameter =
                TypeArgInferenceUtil.substitute(substitutions, formalParameter);
        return construct(newArgument, newFormalParameter);
    }

    /** Used to create a new constraint of the same subclass of AFConstraint. */
    protected abstract AFConstraint construct(
            AnnotatedTypeMirror newArgument, AnnotatedTypeMirror newFormalParameter);
}
