/*
 * Copyright (C) 2015-2016 Federico Tomassetti
 * Copyright (C) 2017-2024 The JavaParser Team.
 *
 * This file is part of JavaParser.
 *
 * JavaParser can be used either under the terms of
 * a) the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * b) the terms of the Apache License
 *
 * You should have received a copy of both licenses in LICENCE.LGPL and
 * LICENCE.APACHE. Please refer to those files for details.
 *
 * JavaParser is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 */

package com.github.javaparser.symbolsolver.resolution.typeinference;

import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.LambdaExpr;
import com.github.javaparser.resolution.MethodUsage;
import com.github.javaparser.resolution.TypeSolver;
import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration;
import com.github.javaparser.resolution.declarations.ResolvedTypeParameterDeclaration;
import com.github.javaparser.resolution.logic.FunctionalInterfaceLogic;
import com.github.javaparser.resolution.model.SymbolReference;
import com.github.javaparser.resolution.model.typesystem.LazyType;
import com.github.javaparser.resolution.model.typesystem.ReferenceTypeImpl;
import com.github.javaparser.resolution.types.*;
import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade;
import com.github.javaparser.utils.Pair;
import java.util.*;

/**
 * The term "type" is used loosely in this chapter to include type-like syntax that contains inference variables.
 *
 * Assertions that involve inference
 * variables are assertions about every proper type that can be produced by replacing each inference variable with
 * a proper type.
 *
 * @author Federico Tomassetti
 */
public class TypeHelper {

    /**
     * The term proper type excludes such "types" that mention inference variables.
     */
    public static boolean isProperType(ResolvedType type) {
        if (type instanceof InferenceVariable) {
            return false;
        }
        if (type instanceof ResolvedReferenceType) {
            ResolvedReferenceType referenceType = (ResolvedReferenceType) type;
            return referenceType.typeParametersValues().stream().allMatch(it -> isProperType(it));
        }
        if (type instanceof LazyType) {
            return type.asReferenceType().typeParametersValues().stream().allMatch(it -> isProperType(it));
        }
        if (type instanceof ResolvedWildcard) {
            ResolvedWildcard wildcard = (ResolvedWildcard) type;
            if (wildcard.isBounded()) {
                return isProperType(wildcard.getBoundedType());
            }
            return true;
        }
        if (type.isPrimitive()) {
            return true;
        }
        if (type.isTypeVariable()) {
            // FIXME I am not sure...
            return false;
        }
        if (type.isArray()) {
            return isProperType(type.asArrayType().getComponentType());
        }
        throw new UnsupportedOperationException(type.toString());
    }

    /**
     * see https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.3
     * @param expression
     * @param t
     * @return
     */
    public static boolean isCompatibleInAStrictInvocationContext(Expression expression, ResolvedType t) {
        throw new UnsupportedOperationException();
    }

    /**
     * see https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.3
     * @param expression
     * @param t
     * @return
     */
    public static boolean isCompatibleInALooseInvocationContext(
            TypeSolver typeSolver, Expression expression, ResolvedType t) {
        // throw new UnsupportedOperationException("Unable to determine if " + expression + " is compatible in a loose
        // invocation context with type " + t);
        return isCompatibleInALooseInvocationContext(
                JavaParserFacade.get(typeSolver).getType(expression), t);
    }

    /**
     * see https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.3
     * @param s
     * @param t
     * @return
     */
    public static boolean isCompatibleInALooseInvocationContext(ResolvedType s, ResolvedType t) {
        // Loose invocation contexts allow a more permissive set of conversions, because they are only used for a
        // particular invocation if no applicable declaration can be found using strict invocation contexts. Loose
        // invocation contexts allow the use of one of the following:
        //
        // - an identity conversion (§5.1.1)

        if (s.equals(t)) {
            return true;
        }

        // - a widening primitive conversion (§5.1.2)

        if (s.isPrimitive() && t.isPrimitive() && areCompatibleThroughWideningPrimitiveConversion(s, t)) {
            return true;
        }

        // - a widening reference conversion (§5.1.5)

        if (s.isReferenceType() && t.isReferenceType() && areCompatibleThroughWideningReferenceConversion(s, t)) {
            return true;
        }

        // - a boxing conversion (§5.1.7) optionally followed by widening reference conversion

        if (s.isPrimitive()
                && t.isReferenceType()
                && areCompatibleThroughWideningReferenceConversion(toBoxedType(s.asPrimitive()), t)) {
            return true;
        }

        // - an unboxing conversion (§5.1.8) optionally followed by a widening primitive conversion

        if (s.isReferenceType()
                && s.asReferenceType().isUnboxable()
                && t.isPrimitive()
                && areCompatibleThroughWideningPrimitiveConversion(
                        s.asReferenceType().toUnboxedType().get(), t)) {
            return true;
        }

        // If, after the conversions listed for an invocation context have been applied, the resulting type is a raw
        // type (§4.8), an unchecked conversion (§5.1.9) may then be applied.
        //
        // A value of the null type (the null reference is the only such value) may be assigned to any reference type
        if (s.isNull() && t.isReferenceType()) {
            return true;
        }

        // throw new UnsupportedOperationException("isCompatibleInALooseInvocationContext unable to decide on s=" + s +
        // ", t=" + t);
        // TODO FIXME
        return t.isAssignableBy(s);
    }

    public static ResolvedType toBoxedType(ResolvedPrimitiveType primitiveType) {
        throw new UnsupportedOperationException();
    }

    // get the resolved boxed type of the specified primitive type
    public static ResolvedType toBoxedType(ResolvedPrimitiveType primitiveType, TypeSolver typeSolver) {
        SymbolReference<ResolvedReferenceTypeDeclaration> typeDeclaration =
                typeSolver.tryToSolveType(primitiveType.getBoxTypeQName());
        return new ReferenceTypeImpl(typeDeclaration.getCorrespondingDeclaration());
    }

    public static boolean areCompatibleThroughWideningReferenceConversion(ResolvedType s, ResolvedType t) {
        Optional<ResolvedPrimitiveType> correspondingPrimitiveTypeForS = Arrays.stream(ResolvedPrimitiveType.values())
                .filter(pt -> pt.getBoxTypeQName().equals(s.asReferenceType().getQualifiedName()))
                .findFirst();
        if (!correspondingPrimitiveTypeForS.isPresent()) {
            return false;
        }
        throw new UnsupportedOperationException("areCompatibleThroughWideningReferenceConversion s=" + s + ", t=" + t);
    }

    public static boolean areCompatibleThroughWideningPrimitiveConversion(ResolvedType s, ResolvedType t) {
        if (s.isPrimitive() && t.isPrimitive()) {
            return s.isAssignableBy(t);
        }
        return false;
    }

    public static Set<InferenceVariable> usedInferenceVariables(ResolvedType type) {
        if (type.isInferenceVariable()) {
            return new HashSet<>(Arrays.asList((InferenceVariable) type));
        }
        if (type.isReferenceType()) {
            Set<InferenceVariable> res = new HashSet<>();
            for (ResolvedType tp : type.asReferenceType().typeParametersValues()) {
                res.addAll(usedInferenceVariables(tp));
            }
            return res;
        }
        throw new UnsupportedOperationException(type.toString());
    }

    /**
     * See JLS 4.10.4. Least Upper Bound.
     * The least upper bound, or "lub", of a set of reference types is a shared supertype that is more specific than
     * any other shared supertype (that is, no other shared supertype is a subtype of the least upper bound).
     */
    public static ResolvedType leastUpperBound(Set<ResolvedType> types) {
        LeastUpperBoundLogic logic = LeastUpperBoundLogic.of();
        return logic.lub(types);
    }

    /**
     * See JLS 15.27.3. Type of a Lambda Expression
     * @return
     */
    public static Pair<ResolvedType, Boolean> groundTargetTypeOfLambda(
            LambdaExpr lambdaExpr, ResolvedType T, TypeSolver typeSolver) {
        // The ground target type is derived from T as follows:
        //
        boolean used18_5_3 = false;

        boolean wildcardParameterized =
                T.asReferenceType().typeParametersValues().stream().anyMatch(tp -> tp.isWildcard());
        if (wildcardParameterized) {
            // - If T is a wildcard-parameterized functional interface type and the lambda expression is explicitly
            // typed,
            //   then the ground target type is inferred as described in §18.5.3.

            if (lambdaExpr.isExplicitlyTyped()) {
                used18_5_3 = true;
                throw new UnsupportedOperationException();
            }
            return new Pair<>(nonWildcardParameterizationOf(T.asReferenceType(), typeSolver), used18_5_3);
        }

        // - Otherwise, the ground target type is T.
        return new Pair<>(T, used18_5_3);
    }

    /**
     * See JLS 9.9
     */
    private static ResolvedReferenceType nonWildcardParameterizationOf(
            ResolvedReferenceType originalType, TypeSolver typeSolver) {
        ResolvedReferenceTypeDeclaration originalTypeDeclaration = originalType
                .getTypeDeclaration()
                .orElseThrow(() -> new RuntimeException("TypeDeclaration unexpectedly empty."));

        List<ResolvedType> TIs = new LinkedList<>();
        List<ResolvedType> AIs = originalType.typeParametersValues();
        List<ResolvedTypeParameterDeclaration> TPs = originalTypeDeclaration.getTypeParameters();

        // Let P1...Pn be the type parameters of I with corresponding bounds B1...Bn. For all i (1 ≤ i ≤ n),
        // Ti is derived according to the form of Ai:

        ResolvedReferenceType object = new ReferenceTypeImpl(typeSolver.getSolvedJavaLangObject());

        for (int i = 0; i < AIs.size(); i++) {
            ResolvedType Ai = AIs.get(i);
            ResolvedType Ti = null;

            // - If Ai is a type, then Ti = Ai.

            if (!Ai.isWildcard()) {
                Ti = Ai;
            }

            // - If Ai is a wildcard, and the corresponding type parameter's bound, Bi, mentions one of P1...Pn, then
            //   Ti is undefined and there is no function type.

            if (Ti == null && Ai.isWildcard() && Ai.asWildcard().mention(originalTypeDeclaration.getTypeParameters())) {
                throw new IllegalArgumentException();
            }

            // - Otherwise:

            if (Ti == null) {

                ResolvedType Bi = TPs.get(i).hasLowerBound() ? TPs.get(i).getLowerBound() : object;

                //   - If Ai is an unbound wildcard ?, then Ti = Bi.

                if (Ai.isWildcard() && !Ai.asWildcard().isBounded()) {
                    Ti = Bi;
                }

                //   - If Ai is a upper-bounded wildcard ? extends Ui, then Ti = glb(Ui, Bi) (§5.1.10).

                else if (Ai.isWildcard() && Ai.asWildcard().isUpperBounded()) {
                    ResolvedType Ui = Ai.asWildcard().getBoundedType();
                    Ti = glb(new HashSet<>(Arrays.asList(Ui, Bi)));
                }

                //   - If Ai is a lower-bounded wildcard ? super Li, then Ti = Li.

                else if (Ai.isWildcard() && Ai.asWildcard().isLowerBounded()) {
                    Ti = Ai.asWildcard().getBoundedType();
                } else {
                    throw new RuntimeException("This should not happen");
                }
            }

            TIs.add(Ti);
        }

        return new ReferenceTypeImpl(originalTypeDeclaration, TIs);
    }

    public static MethodType getFunctionType(ResolvedType type) {
        Optional<MethodUsage> mu = FunctionalInterfaceLogic.getFunctionalMethod(type);
        if (mu.isPresent()) {
            return MethodType.fromMethodUsage(mu.get());
        }
        throw new IllegalArgumentException();
    }

    /**
     * See JLS 5.1.10. Capture Conversion.
     */
    public static ResolvedType glb(Set<ResolvedType> types) {
        if (types.isEmpty()) {
            throw new IllegalArgumentException();
        }
        if (types.size() == 1) {
            return types.iterator().next();
        }
        return new ResolvedIntersectionType(types);
    }
}
