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

import java.util.List;
import java.util.Set;
import javax.lang.model.type.TypeKind;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
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.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.AbstractAtmComboVisitor;
import org.checkerframework.framework.util.AnnotatedTypes;
import org.checkerframework.javacutil.PluginUtil;

/**
 * FIsAReducer takes an FIsA constraint that is not irreducible (@see AFConstraint.isIrreducible)
 * and reduces it by one step. The resulting constraint may still be reducible.
 *
 * <p>Generally reductions should map to corresponding rules in
 * https://docs.oracle.com/javase/specs/jls/se11/html/jls-15.html#jls-15.12.2.7
 */
public class FIsAReducer implements AFReducer {

    protected final FIsAReducingVisitor visitor;
    private final AnnotatedTypeFactory typeFactory;

    public FIsAReducer(final AnnotatedTypeFactory typeFactory) {
        this.typeFactory = typeFactory;
        this.visitor = new FIsAReducingVisitor();
    }

    @Override
    public boolean reduce(AFConstraint constraint, Set<AFConstraint> newConstraints) {
        if (constraint instanceof FIsA) {
            final FIsA fIsA = (FIsA) constraint;
            visitor.visit(fIsA.formalParameter, fIsA.argument, newConstraints);
            return true;

        } else {
            return false;
        }
    }

    /**
     * Given an FIsA constraint of the form: FIsA( typeFromFormalParameter, typeFromMethodArgument )
     *
     * <p>FIsAReducingVisitor visits the constraint as follows: visit ( typeFromFormalParameter,
     * typeFromMethodArgument, newConstraints )
     *
     * <p>The visit method will determine if the given constraint should either:
     *
     * <ul>
     *   <li>be discarded -- in this case, the visitor just returns
     *   <li>reduced to a simpler constraint or set of constraints -- in this case, the new
     *       constraint or set of constraints is added to newConstraints
     * </ul>
     *
     * From the JLS, in general there are 2 rules that govern F = A constraints: If F = Tj, then the
     * constraint Tj = A is implied. If F = U[], where the type U involves Tj, then if A is an array
     * type V[], or a type variable with an upper bound that is an array type V[], where V is a
     * reference type, this algorithm is applied recursively to the constraint {@code V >> U}.
     * Otherwise, no constraint is implied on Tj.
     *
     * <p>Since both F and A may have component types this visitor delves into their components and
     * applies these rules to the components. However, only one step is taken at a time (i.e. this
     * is not a scanner)
     */
    private class FIsAReducingVisitor extends AbstractAtmComboVisitor<Void, Set<AFConstraint>> {
        @Override
        protected String defaultErrorMessage(
                AnnotatedTypeMirror argument,
                AnnotatedTypeMirror parameter,
                Set<AFConstraint> afConstraints) {
            return "Unexpected FIsA Combination:\n"
                    + "argument="
                    + argument
                    + "\n"
                    + "parameter="
                    + parameter
                    + "\n"
                    + "constraints=[\n"
                    + PluginUtil.join(", ", afConstraints)
                    + "\n]";
        }
        // ------------------------------------------------------------------------
        // Arrays as arguments

        @Override
        public Void visitArray_Array(
                AnnotatedArrayType parameter,
                AnnotatedArrayType argument,
                Set<AFConstraint> constraints) {
            constraints.add(new FIsA(parameter.getComponentType(), argument.getComponentType()));
            return null;
        }

        @Override
        public Void visitArray_Declared(
                AnnotatedArrayType parameter,
                AnnotatedDeclaredType argument,
                Set<AFConstraint> constraints) {
            return null;
        }

        @Override
        public Void visitArray_Null(
                AnnotatedArrayType parameter,
                AnnotatedNullType argument,
                Set<AFConstraint> constraints) {
            return null;
        }

        @Override
        public Void visitArray_Wildcard(
                AnnotatedArrayType parameter,
                AnnotatedWildcardType argument,
                Set<AFConstraint> constraints) {
            constraints.add(new FIsA(parameter, argument.getExtendsBound()));
            return null;
        }

        // ------------------------------------------------------------------------
        // Declared as argument
        @Override
        public Void visitDeclared_Array(
                AnnotatedDeclaredType parameter,
                AnnotatedArrayType argument,
                Set<AFConstraint> constraints) {
            // should this be Array<String> - T[] the new A2F(String, T)
            return null;
        }

        @Override
        public Void visitDeclared_Declared(
                AnnotatedDeclaredType parameter,
                AnnotatedDeclaredType argument,
                Set<AFConstraint> constraints) {
            if (argument.wasRaw() || parameter.wasRaw()) {
                return null;
            }

            AnnotatedDeclaredType argumentAsParam =
                    AnnotatedTypes.castedAsSuper(typeFactory, argument, parameter);
            if (argumentAsParam == null) {
                return null;
            }

            final List<AnnotatedTypeMirror> argTypeArgs = argumentAsParam.getTypeArguments();
            final List<AnnotatedTypeMirror> paramTypeArgs = parameter.getTypeArguments();
            for (int i = 0; i < argTypeArgs.size(); i++) {
                final AnnotatedTypeMirror argTypeArg = argTypeArgs.get(i);
                final AnnotatedTypeMirror paramTypeArg = paramTypeArgs.get(i);

                if (paramTypeArg.getKind() == TypeKind.WILDCARD) {
                    final AnnotatedWildcardType paramWc = (AnnotatedWildcardType) paramTypeArg;

                    if (argTypeArg.getKind() == TypeKind.WILDCARD) {
                        final AnnotatedWildcardType argWc = (AnnotatedWildcardType) argTypeArg;
                        constraints.add(
                                new FIsA(paramWc.getExtendsBound(), argWc.getExtendsBound()));
                        constraints.add(new FIsA(paramWc.getSuperBound(), argWc.getSuperBound()));
                    }

                } else {
                    constraints.add(new FIsA(paramTypeArgs.get(i), argTypeArgs.get(i)));
                }
            }

            return null;
        }

        @Override
        public Void visitDeclared_Null(
                AnnotatedDeclaredType parameter,
                AnnotatedNullType argument,
                Set<AFConstraint> constraints) {
            return null;
        }

        @Override
        public Void visitDeclared_Primitive(
                AnnotatedDeclaredType parameter,
                AnnotatedPrimitiveType argument,
                Set<AFConstraint> constraints) {
            return null;
        }

        @Override
        public Void visitDeclared_Union(
                AnnotatedDeclaredType parameter,
                AnnotatedUnionType argument,
                Set<AFConstraint> constraints) {
            return null; // TODO: NOT SUPPORTED AT THE MOMENT
        }

        @Override
        public Void visitIntersection_Intersection(
                AnnotatedIntersectionType parameter,
                AnnotatedIntersectionType argument,
                Set<AFConstraint> constraints) {
            return null; // TODO: NOT SUPPORTED AT THE MOMENT
        }

        @Override
        public Void visitIntersection_Null(
                AnnotatedIntersectionType parameter,
                AnnotatedNullType argument,
                Set<AFConstraint> constraints) {
            return null;
        }

        @Override
        public Void visitNull_Null(
                AnnotatedNullType parameter,
                AnnotatedNullType argument,
                Set<AFConstraint> afConstraints) {
            // we sometimes get these when we have captured types passed as arguments
            // regardless they don't give any information
            return null;
        }

        // ------------------------------------------------------------------------
        // Primitive as argument
        @Override
        public Void visitPrimitive_Declared(
                AnnotatedPrimitiveType parameter,
                AnnotatedDeclaredType argument,
                Set<AFConstraint> constraints) {
            // we may be able to eliminate this case, since I believe the corresponding constraint
            // will just be discarded as the parameter must be a boxed primitive
            constraints.add(new FIsA(typeFactory.getBoxedType(parameter), argument));
            return null;
        }

        @Override
        public Void visitPrimitive_Primitive(
                AnnotatedPrimitiveType parameter,
                AnnotatedPrimitiveType argument,
                Set<AFConstraint> constraints) {
            return null;
        }

        @Override
        public Void visitTypevar_Typevar(
                AnnotatedTypeVariable parameter,
                AnnotatedTypeVariable argument,
                Set<AFConstraint> constraints) {
            // if we've reached this point and the two are corresponding type variables, then they
            // are NOT ones that may have a type variable we are inferring types for and therefore
            // we can discard this constraint
            return null;
        }

        @Override
        public Void visitTypevar_Null(
                AnnotatedTypeVariable argument,
                AnnotatedNullType parameter,
                Set<AFConstraint> constraints) {
            return null;
        }
    }
}
