/*
 * 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.javaparsermodel.contexts;

import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.NameExpr;
import com.github.javaparser.resolution.Context;
import com.github.javaparser.resolution.MethodUsage;
import com.github.javaparser.resolution.TypeSolver;
import com.github.javaparser.resolution.UnsolvedSymbolException;
import com.github.javaparser.resolution.declarations.*;
import com.github.javaparser.resolution.logic.MethodResolutionLogic;
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.symbolsolver.resolution.typeinference.LeastUpperBoundLogic;
import com.github.javaparser.utils.Pair;
import java.util.*;
import java.util.stream.Collectors;

public class MethodCallExprContext extends ExpressionContext<MethodCallExpr> {

    ///
    /// Constructors
    ///

    public MethodCallExprContext(MethodCallExpr wrappedNode, TypeSolver typeSolver) {
        super(wrappedNode, typeSolver);
    }

    ///
    /// Public methods
    ///

    @Override
    public Optional<ResolvedType> solveGenericType(String name) {
        Optional<Expression> nodeScope = wrappedNode.getScope();
        if (!nodeScope.isPresent()) {
            return Optional.empty();
        }

        // Method calls can have generic types defined, for example: {@code expr.<T1, T2>method(x, y, z);} or {@code
        // super.<T, E>check2(val1, val2).}
        ResolvedType typeOfScope = JavaParserFacade.get(typeSolver).getType(nodeScope.get());
        Optional<ResolvedType> resolvedType = typeOfScope.asReferenceType().getGenericParameterByName(name);

        // TODO/FIXME: Consider if we should check if the result is present, else delegate "up" the context chain (e.g.
        // {@code solveGenericTypeInParent()})
        return resolvedType;
    }

    @Override
    public String toString() {
        return "MethodCallExprContext{wrapped=" + wrappedNode + "}";
    }

    @Override
    public Optional<MethodUsage> solveMethodAsUsage(String name, List<ResolvedType> argumentsTypes) {
        ResolvedType typeOfScope;
        if (wrappedNode.hasScope()) {
            Expression scope = wrappedNode.getScope().get();
            // Consider static method calls
            if (scope instanceof NameExpr) {
                String className = ((NameExpr) scope).getName().getId();
                SymbolReference<ResolvedTypeDeclaration> ref = solveType(className);
                if (ref.isSolved()) {
                    SymbolReference<ResolvedMethodDeclaration> m = MethodResolutionLogic.solveMethodInType(
                            ref.getCorrespondingDeclaration(), name, argumentsTypes);
                    if (m.isSolved()) {
                        MethodUsage methodUsage = new MethodUsage(m.getCorrespondingDeclaration());
                        methodUsage = resolveMethodTypeParametersFromExplicitList(typeSolver, methodUsage);
                        methodUsage = resolveMethodTypeParameters(methodUsage, argumentsTypes);
                        return Optional.of(methodUsage);
                    }
                    throw new UnsolvedSymbolException(
                            ref.getCorrespondingDeclaration().toString(),
                            "Method '" + name + "' with parameterTypes " + argumentsTypes);
                }
            }

            // Scope is present -- search/solve within that type
            typeOfScope = JavaParserFacade.get(typeSolver).getType(scope);
        } else {
            // Scope not present -- search/solve within itself.
            typeOfScope = JavaParserFacade.get(typeSolver).getTypeOfThisIn(wrappedNode);
        }

        // we can replace the parameter types from the scope into the typeParametersValues
        Map<ResolvedTypeParameterDeclaration, ResolvedType> inferredTypes = new HashMap<>();
        for (int i = 0; i < argumentsTypes.size(); i++) {
            // by replacing types I can also find new equivalences
            // for example if I replace T=U with String because I know that T=String I can derive that also U equal
            // String
            ResolvedType originalArgumentType = argumentsTypes.get(i);
            ResolvedType updatedArgumentType =
                    usingParameterTypesFromScope(typeOfScope, originalArgumentType, inferredTypes);
            argumentsTypes.set(i, updatedArgumentType);
        }
        for (int i = 0; i < argumentsTypes.size(); i++) {
            ResolvedType updatedArgumentType = applyInferredTypes(argumentsTypes.get(i), inferredTypes);
            argumentsTypes.set(i, updatedArgumentType);
        }

        return solveMethodAsUsage(typeOfScope, name, argumentsTypes, this);
    }

    private MethodUsage resolveMethodTypeParametersFromExplicitList(TypeSolver typeSolver, MethodUsage methodUsage) {
        if (wrappedNode.getTypeArguments().isPresent()) {
            final List<ResolvedType> typeArguments = new ArrayList<>();
            for (com.github.javaparser.ast.type.Type ty :
                    wrappedNode.getTypeArguments().get()) {
                typeArguments.add(JavaParserFacade.get(typeSolver).convertToUsage(ty));
            }

            List<ResolvedTypeParameterDeclaration> tyParamDecls =
                    methodUsage.getDeclaration().getTypeParameters();
            if (tyParamDecls.size() == typeArguments.size()) {
                for (int i = 0; i < tyParamDecls.size(); i++) {
                    methodUsage = methodUsage.replaceTypeParameter(tyParamDecls.get(i), typeArguments.get(i));
                }
            }
        }

        return methodUsage;
    }

    @Override
    public SymbolReference<ResolvedMethodDeclaration> solveMethod(
            String name, List<ResolvedType> argumentsTypes, boolean staticOnly) {
        Collection<ResolvedReferenceTypeDeclaration> rrtds = findTypeDeclarations(wrappedNode.getScope());

        if (rrtds.isEmpty()) {
            // if the bounds of a type parameter are empty, then the bound is implicitly "extends Object"
            // we don't make this _ex_plicit in the data representation because that would affect codegen
            // and make everything generate like <T extends Object> instead of <T>
            // https://github.com/javaparser/javaparser/issues/2044
            rrtds = Collections.singleton(typeSolver.getSolvedJavaLangObject());
        }

        for (ResolvedReferenceTypeDeclaration rrtd : rrtds) {
            SymbolReference<ResolvedMethodDeclaration> res =
                    MethodResolutionLogic.solveMethodInType(rrtd, name, argumentsTypes, false);
            if (res.isSolved()) {
                return res;
            }
        }

        return SymbolReference.unsolved();
    }

    ///
    /// Private methods
    ///

    private Optional<MethodUsage> solveMethodAsUsage(
            ResolvedReferenceType refType, String name, List<ResolvedType> argumentsTypes, Context invokationContext) {
        if (!refType.getTypeDeclaration().isPresent()) {
            return Optional.empty();
        }

        Optional<MethodUsage> ref = ContextHelper.solveMethodAsUsage(
                refType.getTypeDeclaration().get(),
                name,
                argumentsTypes,
                invokationContext,
                refType.typeParametersValues());
        if (ref.isPresent()) {
            MethodUsage methodUsage = ref.get();

            methodUsage = resolveMethodTypeParametersFromExplicitList(typeSolver, methodUsage);

            // At this stage I should derive from the context and the value some information on the type parameters
            // for example, when calling:
            // myStream.collect(Collectors.toList())
            // I should be able to figure out that considering the type of the stream (e.g., Stream<String>)
            // and considering that Stream has this method:
            //
            // <R,A> R collect(Collector<? super T,A,R> collector)
            //
            // and collector has this method:
            //
            // static <T> Collector<T,?,List<T>>   toList()
            //
            // In this case collect.R has to be equal to List<toList.T>
            // And toList.T has to be equal to ? super Stream.T
            // Therefore R has to be equal to List<? super Stream.T>.
            // In our example Stream.T equal to String, so the R (and the result of the call to collect) is
            // List<? super String>

            Map<ResolvedTypeParameterDeclaration, ResolvedType> derivedValues = new HashMap<>();
            for (int i = 0; i < methodUsage.getParamTypes().size(); i++) {
                ResolvedParameterDeclaration parameter =
                        methodUsage.getDeclaration().getParam(i);
                ResolvedType parameterType = parameter.getType();
                // Don't continue if a vararg parameter is reached and there are no arguments left
                if (parameter.isVariadic() && argumentsTypes.size() < methodUsage.getNoParams()) {
                    break;
                }
                if (!argumentsTypes.get(i).isArray() && parameter.isVariadic()) {
                    parameterType = parameterType.asArrayType().getComponentType();
                }
                MethodResolutionLogic.inferTypes(argumentsTypes.get(i), parameterType, derivedValues);
            }

            for (Map.Entry<ResolvedTypeParameterDeclaration, ResolvedType> entry : derivedValues.entrySet()) {
                methodUsage = methodUsage.replaceTypeParameter(entry.getKey(), entry.getValue());
            }

            ResolvedType returnType = refType.useThisTypeParametersOnTheGivenType(methodUsage.returnType());
            // we don't want to replace the return type in case of UNBOUNDED type (<?>)
            if (returnType != methodUsage.returnType() && !(returnType == ResolvedWildcard.UNBOUNDED)) {
                methodUsage = methodUsage.replaceReturnType(returnType);
            }
            for (int i = 0; i < methodUsage.getParamTypes().size(); i++) {
                ResolvedType replaced = refType.useThisTypeParametersOnTheGivenType(
                        methodUsage.getParamTypes().get(i));
                methodUsage = methodUsage.replaceParamType(i, replaced);
            }
            return Optional.of(methodUsage);
        }
        return ref;
    }

    private MethodUsage resolveMethodTypeParameters(MethodUsage methodUsage, List<ResolvedType> actualParamTypes) {
        Map<ResolvedTypeParameterDeclaration, ResolvedType> matchedTypeParameters = new HashMap<>();

        if (methodUsage.getDeclaration().hasVariadicParameter()) {
            if (actualParamTypes.size() == methodUsage.getDeclaration().getNumberOfParams()) {
                // the varargs parameter is an Array, so extract the inner type
                ResolvedType expectedType = methodUsage
                        .getDeclaration()
                        .getLastParam()
                        .getType()
                        .asArrayType()
                        .getComponentType();
                // the varargs corresponding type can be either T or Array<T>
                // for example
                // Arrays.aslist(int[]{1}) must returns List<int[]>
                // but Arrays.aslist(String[]{""}) must returns List<String>
                // Arrays.asList() accept generic type T. Since Java generics work only on
                // reference types (object types), not on primitives, and int[] is an object
                // then Arrays.aslist(int[]{1}) returns List<int[]>
                ResolvedType lastActualParamType = actualParamTypes.get(actualParamTypes.size() - 1);
                ResolvedType actualType = lastActualParamType;
                if (lastActualParamType.isArray()) {
                    ResolvedType componentType =
                            lastActualParamType.asArrayType().getComponentType();
                    // in cases where, the expected type is assignable by the actual reference type of the array
                    // (Files.newInputStream(path, options) and options is a variadic argument of type OpenOption)
                    // or the expected type is a generic type (Arrays.asList(T... a)) and the component type of the
                    // array type is a reference type
                    // or the expected type is not a generic (IntStream.of(int... values)) and the component type is not
                    // a reference type
                    // then the actual type is the component type (in the example above 'int')
                    if ((componentType.isReferenceType() && ResolvedTypeVariable.class.isInstance(expectedType))
                            || (!componentType.isReferenceType()
                                    && !ResolvedTypeVariable.class.isInstance(expectedType))
                            || (componentType.isReferenceType() && expectedType.isAssignableBy(componentType))) {
                        actualType = lastActualParamType.asArrayType().getComponentType();
                    }
                }
                if (!expectedType.isAssignableBy(actualType)) {
                    for (ResolvedTypeParameterDeclaration tp :
                            methodUsage.getDeclaration().getTypeParameters()) {
                        expectedType = MethodResolutionLogic.replaceTypeParam(expectedType, tp, typeSolver);
                    }
                }
                if (!expectedType.isAssignableBy(actualType)) {
                    // ok, then it needs to be wrapped
                    throw new UnsupportedOperationException(String.format(
                            "Unable to resolve the type typeParametersValues in a MethodUsage. Expected type: %s, Actual type: %s. Method Declaration: %s. MethodUsage: %s",
                            expectedType, actualType, methodUsage.getDeclaration(), methodUsage));
                }
                // match only the varargs type
                matchTypeParameters(expectedType, actualType, matchedTypeParameters);
            } else if (methodUsage.getDeclaration().getNumberOfParams() == 1) {
                // In this case the method declares only one parameter which is a variadic parameter.
                // At this stage we can consider that the actual parameters all have the same type.
                ResolvedType expectedType = methodUsage
                        .getDeclaration()
                        .getLastParam()
                        .getType()
                        .asArrayType()
                        .getComponentType();
                // the varargs corresponding type can not be an Array<T> because of the assumption
                // ResolvedType actualType = new ResolvedArrayType(actualParamTypes.get(actualParamTypes.size() - 1));
                // It is possible that the method is called with no arguments, in which case we can't get any further
                // type information about the parameters
                if (actualParamTypes.isEmpty()) {
                    return methodUsage;
                }
                ResolvedType actualType = actualParamTypes.get(actualParamTypes.size() - 1);
                if (!expectedType.isAssignableBy(actualType)) {
                    throw new UnsupportedOperationException(String.format(
                            "Unable to resolve the type typeParametersValues in a MethodUsage. Expected type: %s, Actual type: %s. Method Declaration: %s. MethodUsage: %s",
                            expectedType, actualType, methodUsage.getDeclaration(), methodUsage));
                }
                matchTypeParameters(expectedType, actualType, matchedTypeParameters);
                return replaceTypeParameter(methodUsage, matchedTypeParameters);
            } else {
                return methodUsage;
            }
        }

        int until = methodUsage.getDeclaration().hasVariadicParameter()
                ? actualParamTypes.size() - 1
                : actualParamTypes.size();

        for (int i = 0; i < until; i++) {
            ResolvedType expectedType = methodUsage.getParamType(i);
            ResolvedType actualType = actualParamTypes.get(i);
            matchTypeParameters(expectedType, actualType, matchedTypeParameters);
        }
        methodUsage = replaceTypeParameter(methodUsage, matchedTypeParameters);
        return methodUsage;
    }

    /*
     * Replace formal type parameter (e.g. T) by actual type argument
     * If there is more than one actual type argument for a formal type parameter
     * then the type parameter list is reduced using LUB fonction.
     */
    private MethodUsage replaceTypeParameter(
            MethodUsage methodUsage, Map<ResolvedTypeParameterDeclaration, ResolvedType> matchedTypeParameters) {
        // first group all resolved types by type variable
        Map<String, Set<ResolvedType>> resolvedTypesByTypeVariable =
                groupResolvedTypeByTypeVariable(matchedTypeParameters);
        // then reduce the list of resolved types with the least upper bound logic
        Map<String, ResolvedType> reducedResolvedTypesByTypeVariable =
                reduceResolvedTypesByTypeVariable(resolvedTypesByTypeVariable);
        // then replace resolved type by the reduced type for each type variable
        convertTypesParameters(matchedTypeParameters, reducedResolvedTypesByTypeVariable);
        // finally replace type parameters
        for (ResolvedTypeParameterDeclaration tp : matchedTypeParameters.keySet()) {
            methodUsage = methodUsage.replaceTypeParameter(tp, matchedTypeParameters.get(tp));
        }
        return methodUsage;
    }

    /*
     * Update the matchedTypeParameters map from the types in reducedResolvedTypesByTypeVariable map.
     */
    private void convertTypesParameters(
            Map<ResolvedTypeParameterDeclaration, ResolvedType> matchedTypeParameters,
            Map<String, ResolvedType> reducedResolvedTypesByTypeVariable) {
        for (ResolvedTypeParameterDeclaration tp : matchedTypeParameters.keySet()) {
            String typeParameterName = tp.getName();
            boolean replacement = reducedResolvedTypesByTypeVariable.keySet().contains(typeParameterName);
            if (replacement) {
                matchedTypeParameters.put(tp, reducedResolvedTypesByTypeVariable.get(typeParameterName));
            }
        }
    }

    /*
     * Group resolved type by the variable type. For example in Map.of("k0", 0, "k1",
     * 1D) which is solved as static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2)
     * the type variable named V that represents the type of the first and fourth parameter
     * must reference v1 (Integer type) and v2 (Double type).
     */
    private Map<String, Set<ResolvedType>> groupResolvedTypeByTypeVariable(
            Map<ResolvedTypeParameterDeclaration, ResolvedType> typeParameters) {
        Map<String, Set<ResolvedType>> resolvedTypesByTypeVariable = new HashMap<>();
        for (ResolvedTypeParameterDeclaration tp : typeParameters.keySet()) {
            String typeParameterName = tp.getName();
            boolean alreadyCollected = resolvedTypesByTypeVariable.keySet().contains(typeParameterName);
            if (!alreadyCollected) {
                Set<ResolvedType> resolvedTypes = findResolvedTypesByTypeVariable(typeParameterName, typeParameters);
                resolvedTypesByTypeVariable.put(typeParameterName, resolvedTypes);
            }
        }
        return resolvedTypesByTypeVariable;
    }

    /*
     * Collect all resolved type from a type variable name
     */
    private Set<ResolvedType> findResolvedTypesByTypeVariable(
            String typeVariableName, Map<ResolvedTypeParameterDeclaration, ResolvedType> typeParameters) {
        return typeParameters.keySet().stream()
                .filter(resolvedTypeParameterDeclaration ->
                        resolvedTypeParameterDeclaration.getName().equals(typeVariableName))
                .map(resolvedTypeParameterDeclaration -> typeParameters.get(resolvedTypeParameterDeclaration))
                .collect(Collectors.toSet());
    }

    /*
     * Reduce all set of resolved type with LUB
     */
    private Map<String, ResolvedType> reduceResolvedTypesByTypeVariable(Map<String, Set<ResolvedType>> typeParameters) {
        Map<String, ResolvedType> reducedResolvedTypesList = new HashMap<>();
        for (String typeParameterName : typeParameters.keySet()) {
            ResolvedType type = reduceResolvedTypesWithLub(typeParameters.get(typeParameterName));
            reducedResolvedTypesList.put(typeParameterName, type);
        }
        return reducedResolvedTypesList;
    }

    private ResolvedType reduceResolvedTypesWithLub(Set<ResolvedType> resolvedTypes) {
        return LeastUpperBoundLogic.of().lub(resolvedTypes);
    }

    private void matchTypeParameters(
            ResolvedType expectedType,
            ResolvedType actualType,
            Map<ResolvedTypeParameterDeclaration, ResolvedType> matchedTypeParameters) {
        if (expectedType.isTypeVariable()) {
            ResolvedType type = actualType;
            // in case of primitive type, the expected type must be compared with the boxed type of the actual type
            if (type.isPrimitive()) {
                ResolvedReferenceTypeDeclaration resolvedTypedeclaration =
                        typeSolver.solveType(type.asPrimitive().getBoxTypeQName());
                type = new ReferenceTypeImpl(resolvedTypedeclaration);
            }
            /*
             * "a value of the null type (the null reference is the only such value) may be assigned to any reference type, resulting in a null reference of that type"
             * https://docs.oracle.com/javase/specs/jls/se15/html/jls-5.html#jls-5.2
             */
            if (type.isNull()) {
                ResolvedReferenceTypeDeclaration resolvedTypedeclaration = typeSolver.getSolvedJavaLangObject();
                type = new ReferenceTypeImpl(resolvedTypedeclaration);
            }
            if (!type.isTypeVariable() && !type.isReferenceType() && !type.isArray()) {
                throw new UnsupportedOperationException(type.getClass().getCanonicalName());
            }
            matchedTypeParameters.put(expectedType.asTypeParameter(), type);
        } else if (expectedType.isArray()) {
            // Issue 2258 : NullType must not fail this search
            if (!(actualType.isArray() || actualType.isNull())) {
                throw new UnsupportedOperationException(actualType.getClass().getCanonicalName());
            }
            matchTypeParameters(
                    expectedType.asArrayType().getComponentType(),
                    actualType.isNull() ? actualType : actualType.asArrayType().getComponentType(),
                    matchedTypeParameters);
        } else if (expectedType.isReferenceType()) {
            // avoid cases where the actual type has no type parameters but the expected one has. Such as: "classX
            // extends classY<Integer>"
            if (actualType.isReferenceType()
                    && actualType.asReferenceType().typeParametersValues().size() > 0) {
                int i = 0;
                for (ResolvedType tp : expectedType.asReferenceType().typeParametersValues()) {
                    matchTypeParameters(
                            tp,
                            actualType.asReferenceType().typeParametersValues().get(i),
                            matchedTypeParameters);
                    i++;
                }
            }
        } else if (expectedType.isPrimitive()) {
            // nothing to do
        } else if (expectedType.isWildcard()) {
            // nothing to do
        } else {
            throw new UnsupportedOperationException(expectedType.getClass().getCanonicalName());
        }
    }

    private Optional<MethodUsage> solveMethodAsUsage(
            ResolvedTypeVariable tp, String name, List<ResolvedType> argumentsTypes, Context invokationContext) {
        List<ResolvedTypeParameterDeclaration.Bound> bounds =
                tp.asTypeParameter().getBounds();

        if (bounds.isEmpty()) {
            // if the bounds of a type parameter are empty, then the bound is implicitly "extends Object"
            // we don't make this _ex_plicit in the data representation because that would affect codegen
            // and make everything generate like <T extends Object> instead of <T>
            // https://github.com/javaparser/javaparser/issues/2044
            bounds = Collections.singletonList(ResolvedTypeParameterDeclaration.Bound.extendsBound(
                    new ReferenceTypeImpl(typeSolver.getSolvedJavaLangObject())));
            ;
        }

        for (ResolvedTypeParameterDeclaration.Bound bound : bounds) {
            Optional<MethodUsage> methodUsage =
                    solveMethodAsUsage(bound.getType(), name, argumentsTypes, invokationContext);
            if (methodUsage.isPresent()) {
                return methodUsage;
            }
        }

        return Optional.empty();
    }

    private Optional<MethodUsage> solveMethodAsUsage(
            ResolvedType type, String name, List<ResolvedType> argumentsTypes, Context invokationContext) {
        if (type instanceof ResolvedReferenceType) {
            return solveMethodAsUsage((ResolvedReferenceType) type, name, argumentsTypes, invokationContext);
        }
        if (type instanceof LazyType) {
            return solveMethodAsUsage(type.asReferenceType(), name, argumentsTypes, invokationContext);
        }
        if (type instanceof ResolvedTypeVariable) {
            return solveMethodAsUsage((ResolvedTypeVariable) type, name, argumentsTypes, invokationContext);
        }
        if (type instanceof ResolvedWildcard) {
            ResolvedWildcard wildcardUsage = (ResolvedWildcard) type;
            if (wildcardUsage.isSuper()) {
                return solveMethodAsUsage(wildcardUsage.getBoundedType(), name, argumentsTypes, invokationContext);
            }
            if (wildcardUsage.isExtends()) {
                return solveMethodAsUsage(wildcardUsage.getBoundedType(), name, argumentsTypes, invokationContext);
            }
            return solveMethodAsUsage(
                    new ReferenceTypeImpl(typeSolver.getSolvedJavaLangObject()),
                    name,
                    argumentsTypes,
                    invokationContext);
        }
        if (type instanceof ResolvedLambdaConstraintType) {
            ResolvedLambdaConstraintType constraintType = (ResolvedLambdaConstraintType) type;
            return solveMethodAsUsage(constraintType.getBound(), name, argumentsTypes, invokationContext);
        }
        if (type instanceof ResolvedArrayType) {
            // An array inherits methods from Object not from it's component type
            return solveMethodAsUsage(
                    new ReferenceTypeImpl(typeSolver.getSolvedJavaLangObject()),
                    name,
                    argumentsTypes,
                    invokationContext);
        }
        if (type instanceof ResolvedUnionType) {
            Optional<ResolvedReferenceType> commonAncestor = type.asUnionType().getCommonAncestor();
            if (commonAncestor.isPresent()) {
                return solveMethodAsUsage(commonAncestor.get(), name, argumentsTypes, invokationContext);
            }
            throw new UnsupportedOperationException("no common ancestor available for " + type.describe());
        }
        throw new UnsupportedOperationException("type usage: " + type.getClass().getCanonicalName());
    }

    private ResolvedType usingParameterTypesFromScope(
            ResolvedType scope, ResolvedType type, Map<ResolvedTypeParameterDeclaration, ResolvedType> inferredTypes) {
        if (type.isReferenceType()) {
            for (Pair<ResolvedTypeParameterDeclaration, ResolvedType> entry :
                    type.asReferenceType().getTypeParametersMap()) {
                if (entry.a.declaredOnType()
                        && scope.isReferenceType()
                        && scope.asReferenceType()
                                .getGenericParameterByName(entry.a.getName())
                                .isPresent()) {
                    type = type.replaceTypeVariables(
                            entry.a,
                            scope.asReferenceType()
                                    .getGenericParameterByName(entry.a.getName())
                                    .get(),
                            inferredTypes);
                }
            }
        }
        return type;
    }

    private ResolvedType applyInferredTypes(
            ResolvedType type, Map<ResolvedTypeParameterDeclaration, ResolvedType> inferredTypes) {
        for (ResolvedTypeParameterDeclaration tp : inferredTypes.keySet()) {
            type = type.replaceTypeVariables(tp, inferredTypes.get(tp), inferredTypes);
        }
        return type;
    }
}
