// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi.impl.source.resolve.graphInference;

import com.intellij.core.JavaPsiBundle;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.resolve.DefaultParameterTypeInferencePolicy;
import com.intellij.psi.impl.source.resolve.ParameterTypeInferencePolicy;
import com.intellij.psi.impl.source.resolve.graphInference.constraints.*;
import com.intellij.psi.infos.MethodCandidateInfo;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.*;
import com.intellij.util.Function;
import com.intellij.util.MathUtil;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.UniqueNameGenerator;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;

import java.util.*;

public class InferenceSession {
  private static final Logger LOG = Logger.getInstance(InferenceSession.class);
  private static final Function<Pair<PsiType, PsiType>, PsiType> UPPER_BOUND_FUNCTION = pair -> GenericsUtil.getGreatestLowerBound(pair.first, pair.second);

  private static final String EQUALITY_CONSTRAINTS_PRESENTATION = "equality constraints";
  private static final String UPPER_BOUNDS_PRESENTATION = "upper bounds";
  private static final String LOWER_BOUNDS_PRESENTATION = "lower bounds";
  static final Key<PsiCapturedWildcardType> ORIGINAL_CAPTURE = Key.create("ORIGINAL_CAPTURE");

  protected final Set<InferenceVariable> myInferenceVariables = new LinkedHashSet<>();
  private final List<ConstraintFormula> myConstraints = new ArrayList<>();
  private final Set<ConstraintFormula> myConstraintsCopy = new HashSet<>();
  private final InferenceSessionContainer myInferenceSessionContainer;

  private PsiSubstitutor mySiteSubstitutor;
  private final PsiManager myManager;
  private int myConstraintIdx;
  
  private List<@Nls String> myErrorMessages;
  
  private boolean myErased;

  public final InferenceIncorporationPhase myIncorporationPhase = new InferenceIncorporationPhase(this);

  private final PsiElement myContext;
  private final ParameterTypeInferencePolicy myPolicy;

  private PsiSubstitutor myInferenceSubstitution = PsiSubstitutor.EMPTY;
  private PsiSubstitutor myRestoreNameSubstitution = PsiSubstitutor.EMPTY;
  private MethodCandidateInfo myCurrentMethod;
  private ThreadLocalTypes myTempTypes;

  public InferenceSession(InitialInferenceState initialState, ParameterTypeInferencePolicy policy) {
    myContext = initialState.getContext();
    myManager = myContext.getManager();

    myInferenceSubstitution = initialState.getInferenceSubstitutor();
    myInferenceVariables.addAll(initialState.getInferenceVariables());
    mySiteSubstitutor = initialState.getSiteSubstitutor();

    for (Pair<InferenceVariable[], PsiClassType> capture : initialState.getCaptures()) {
      myIncorporationPhase.addCapture(capture.first, capture.second);
    }
    myInferenceSessionContainer = initialState.getInferenceSessionContainer();
    myErased = initialState.isErased();
    myPolicy = policy;
  }

  public InferenceSession(PsiTypeParameter[] typeParams,
                          PsiType[] leftTypes,
                          PsiType[] rightTypes,
                          PsiSubstitutor siteSubstitutor,
                          PsiManager manager,
                          PsiElement context) {
    myManager = manager;
    mySiteSubstitutor = siteSubstitutor;
    myContext = context;

    initBounds(typeParams);

    LOG.assertTrue(leftTypes.length == rightTypes.length);
    for (int i = 0; i < leftTypes.length; i++) {
      final PsiType rightType = mySiteSubstitutor.substitute(rightTypes[i]);
      PsiType t = substituteWithInferenceVariables(leftTypes[i]);
      PsiType s = substituteWithInferenceVariables(rightType);
      if (t != null && s != null) {
        addConstraint(new TypeCompatibilityConstraint(t, s));
      }
    }
    myPolicy = DefaultParameterTypeInferencePolicy.INSTANCE;
    myInferenceSessionContainer = new InferenceSessionContainer();
  }

  public InferenceSession(PsiTypeParameter[] typeParams,
                          PsiSubstitutor siteSubstitutor,
                          PsiManager manager,
                          PsiElement context) {
    this(typeParams, siteSubstitutor, manager, context, DefaultParameterTypeInferencePolicy.INSTANCE);
  }

  public InferenceSession(PsiTypeParameter[] typeParams,
                          PsiSubstitutor siteSubstitutor,
                          PsiManager manager,
                          PsiElement context,
                          ParameterTypeInferencePolicy policy) {
    myManager = manager;
    mySiteSubstitutor = siteSubstitutor;
    myContext = context;
    myPolicy = policy;

    initBounds(typeParams);
    myInferenceSessionContainer = new InferenceSessionContainer();
  }

  public MethodCandidateInfo getCurrentMethod(PsiExpressionList argumentList) {
    return myCurrentMethod != null && myCurrentMethod.isOnArgumentList(argumentList) ? myCurrentMethod : null;
  }

  public void setCurrentMethod(MethodCandidateInfo currentMethod) {
    myCurrentMethod = currentMethod;
  }

  public @NotNull ParameterTypeInferencePolicy getInferencePolicy() {
    return myPolicy;
  }

  public static PsiType createTypeParameterTypeWithUpperBound(@NotNull PsiType upperBound, @NotNull PsiElement place) {
    final PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(place.getProject());

    final PsiTypeParameter parameter = elementFactory.createTypeParameterFromText("T", place);
    TypeConversionUtil.setInferredBoundsForSynthetic(parameter, null, upperBound);

    return elementFactory.createType(parameter);
  }

  public void initExpressionConstraints(PsiParameter[] parameters,
                                        PsiExpression[] args,
                                        PsiMethod method,
                                        boolean varargs) {
    if (method != null) {
      initThrowsConstraints(method);
    }
    if (parameters.length > 0) {
      for (int i = 0; i < args.length; i++) {
        //don't infer anything if number of parameters differ and method is not vararg
        if (!varargs && i >= parameters.length) {
          continue;
        }

        if (args[i] != null && isPertinentToApplicability(args[i], method)) {
          PsiType parameterType = getParameterType(parameters, i, mySiteSubstitutor, varargs);
          if (method != null && PsiUtil.isRawSubstitutor(method, mySiteSubstitutor)) {
            parameterType = TypeConversionUtil.erasure(parameterType);
          }
          LOG.assertTrue(parameterType != null);
          if (!ignoreLambdaConstraintTree(args[i])) {
            addConstraint(new ExpressionCompatibilityConstraint(args[i], substituteWithInferenceVariables(parameterType)));
          }
        }
      }
    }
  }

  public void initThrowsConstraints(PsiMethod method) {
    for (PsiClassType thrownType : method.getThrowsList().getReferencedTypes()) {
      final InferenceVariable variable = getInferenceVariable(substituteWithInferenceVariables(thrownType));
      if (variable != null) {
        variable.setThrownBound();
      }
    }
  }
  
  static PsiExpressionList getArgumentList(PsiElement parent) {
    if (parent instanceof PsiCall) {
      return ((PsiCall)parent).getArgumentList();
    }
    if (parent instanceof PsiAnonymousClass) {
      return getArgumentList(parent.getParent());
    }
    return null;
  }

  /**
   * Definition from 15.12.2.2 Phase 1: Identify Matching Arity Methods Applicable by Subtyping Strict Invocation
   * An argument expression is considered pertinent to applicability for a potentially-applicable method m unless it has one of the following forms:

   1)  An implicitly-typed lambda expression (15.27.1).
   2) An inexact method reference (15.13.1).
   3) If m is a generic method and the method invocation does not provide explicit type arguments, an explicitly-typed lambda expression or 
      an exact method reference for which the corresponding target type (as derived from the signature of m) is a type parameter of m.
   4) An explicitly-typed lambda expression whose body is an expression that is not pertinent to applicability.
   5) An explicitly-typed lambda expression whose body is a block, where at least one result expression is not pertinent to applicability.
   6) A parenthesized expression (15.8.5) whose contained expression is not pertinent to applicability.
   7) A conditional expression (15.25) whose second or third operand is not pertinent to applicability. 
  */
  public static boolean isPertinentToApplicability(PsiExpression expr, PsiMethod method) {
    return isPertinentToApplicability(expr, method, null);
  }

  private static boolean isPertinentToApplicability(PsiExpression expr, PsiMethod method, PsiType expectedReturnType) {
    if (expr instanceof PsiLambdaExpression && ((PsiLambdaExpression)expr).hasFormalParameterTypes() ||
        expr instanceof PsiMethodReferenceExpression && ((PsiMethodReferenceExpression)expr).isExact()) {
      if (method != null) {
        final PsiElement parent = PsiUtil.skipParenthesizedExprUp(expr.getParent());
        PsiType paramType = null;
        if (parent instanceof PsiExpressionList) {
          final PsiElement gParent = parent.getParent();
          PsiTypeParameterListOwner owner = getTypeParameterOwner(method, gParent);
          if (owner != null) {
            final int idx = LambdaUtil.getLambdaIdx((PsiExpressionList)parent, expr);
            final PsiParameter[] parameters = method.getParameterList().getParameters();
            if (idx > parameters.length - 1) {
              final PsiType lastParamType = parameters[parameters.length - 1].getType();
              paramType = parameters[parameters.length - 1].isVarArgs() ? ((PsiEllipsisType)lastParamType).getComponentType() : lastParamType;
            }
            else {
              paramType = parameters[idx].getType();
            }
            if (isTypeParameterType(owner, paramType)) return false;
          }
        }
        else if (expectedReturnType != null && (parent instanceof PsiLambdaExpression ||
                                                parent instanceof PsiReturnStatement && PsiTreeUtil.getParentOfType(parent, PsiLambdaExpression.class, true, PsiMethod.class) != null)) {
          if (isTypeParameterType(method, expectedReturnType)) return false;
          paramType = expectedReturnType;
        }

        if (expr instanceof PsiLambdaExpression) {
          for (PsiExpression expression : LambdaUtil.getReturnExpressions((PsiLambdaExpression)expr)) {
            if (!isPertinentToApplicability(expression, method, LambdaUtil.getFunctionalInterfaceReturnType(paramType))) return false;
          }
          return true;
        }
      }
    }
    if (expr instanceof PsiLambdaExpression) {
      return ((PsiLambdaExpression)expr).hasFormalParameterTypes();
    }
    if (expr instanceof PsiMethodReferenceExpression) {
      return ((PsiMethodReferenceExpression)expr).isExact();
    }
    if (expr instanceof PsiParenthesizedExpression) {
      return isPertinentToApplicability(((PsiParenthesizedExpression)expr).getExpression(), method);
    }
    if (expr instanceof PsiConditionalExpression) {
      final PsiExpression thenExpression = ((PsiConditionalExpression)expr).getThenExpression();
      if (!isPertinentToApplicability(thenExpression, method)) return false;
      final PsiExpression elseExpression = ((PsiConditionalExpression)expr).getElseExpression();
      if (!isPertinentToApplicability(elseExpression, method)) return false;
    }
    return true;
  }

  private static PsiTypeParameterListOwner getTypeParameterOwner(@NotNull PsiMethod method, PsiElement gParent) {
    PsiTypeParameterListOwner owner = null;
    if (method.getTypeParameters().length > 0 && gParent instanceof PsiCallExpression && ((PsiCallExpression)gParent).getTypeArgumentList().getTypeParameterElements().length == 0) {
      owner = method;
    }
    else if (method.isConstructor() && gParent instanceof PsiNewExpression) {
      final PsiClass containingClass = method.getContainingClass();
      if (containingClass != null && containingClass.hasTypeParameters() && PsiDiamondType.hasDiamond((PsiNewExpression)gParent)) {
        owner = containingClass;
      }
    }
    return owner;
  }

  private static boolean isTypeParameterType(PsiTypeParameterListOwner method, PsiType paramType) {
    if (paramType instanceof PsiWildcardType) return isTypeParameterType(method, ((PsiWildcardType)paramType).getBound());
    final PsiClass psiClass = PsiUtil.resolveClassInType(paramType); //accept ellipsis here
    if (psiClass instanceof PsiTypeParameter && ((PsiTypeParameter)psiClass).getOwner() == method) return true;
    return false;
  }

  private static PsiType getParameterType(PsiParameter[] parameters, int i, @Nullable PsiSubstitutor substitutor, boolean varargs) {
    if (substitutor == null || !varargs && i >= parameters.length) return null;
    return substitutor.substitute(PsiTypesUtil.getParameterType(parameters, i, varargs));
  }

  public @NotNull PsiSubstitutor infer() {
    return infer(null, null, null, null);
  }


  PsiSubstitutor collectAdditionalAndInfer(PsiParameter @NotNull [] parameters,
                                           PsiExpression @NotNull [] args,
                                           @NotNull MethodCandidateInfo properties,
                                           @NotNull PsiSubstitutor psiSubstitutor) {
    return performGuardedInference(parameters, args, myContext, properties, psiSubstitutor, false);
  }

  public @NotNull PsiSubstitutor infer(PsiParameter @Nullable [] parameters,
                                       PsiExpression @Nullable [] args,
                                       @Nullable PsiElement parent,
                                       @Nullable MethodCandidateInfo currentMethod) {
    return performGuardedInference(parameters, args, parent, currentMethod, PsiSubstitutor.EMPTY, false);
  }

  @NotNull
  PsiSubstitutor performGuardedInference(PsiParameter @Nullable [] parameters,
                                         PsiExpression @Nullable [] args,
                                         @Nullable PsiElement parent,
                                         @Nullable MethodCandidateInfo currentMethod,
                                         @NotNull PsiSubstitutor initialSubstitutor,
                                         boolean prohibitCaching) {
    if (!prohibitCaching) {
      prohibitCaching = MethodCandidateInfo.isOverloadCheck() ||
                        !(parent instanceof PsiMethodCallExpression &&
                          ((PsiMethodCallExpression)parent).multiResolve(false).length == 1);
    }
    return ThreadLocalTypes.performWithTypes(types -> {
      myTempTypes = types;
      try {
        doInfer(parameters, args, parent, currentMethod, initialSubstitutor);
        return prepareSubstitution();
      }
      finally {
        if (currentMethod != null) {
          if (myErrorMessages != null) {
            currentMethod.setApplicabilityError(StringUtil.join(myErrorMessages, "\n"));
          }
          if (myErased) {
            currentMethod.setErased();
          }
        }
        myTempTypes = null;
      }
    }, prohibitCaching);
  }

  private void doInfer(PsiParameter @Nullable [] parameters,
                       PsiExpression @Nullable [] args,
                       @Nullable PsiElement parent,
                       @Nullable MethodCandidateInfo properties,
                       @NotNull PsiSubstitutor initialSubstitutor) {
    if (!repeatInferencePhases()) {
      return;
    }

    PsiExpressionList argumentList = getArgumentList(parent);
    if (properties != null && argumentList != null && !MethodCandidateInfo.isOverloadCheck(argumentList)) {
      final PsiMethod method = properties.getElement();
      if (parent instanceof PsiCallExpression && PsiPolyExpressionUtil.isMethodCallPolyExpression((PsiExpression)parent, method)) {
        final PsiType returnType = method.getReturnType();
        if (!PsiTypes.voidType().equals(returnType) && returnType != null) {
          final Ref<String> errorMessage = new Ref<>();
          final PsiType targetType = getTargetTypeFromParent(parent, errorMessage, false);
          if (targetType == null && errorMessage.get() != null) {
            return;
          }

          if (targetType != null && !PsiTypes.voidType().equals(targetType)) {
            PsiType actualType = PsiUtil.isRawSubstitutor(method, mySiteSubstitutor) ? returnType : mySiteSubstitutor.substitute(returnType);
            registerReturnTypeConstraints(actualType, targetType, myContext);
          }
        }
      }

      if (!repeatInferencePhases()) {
        if (isPertinentToApplicabilityCheckOnContainingCall(parent)) {
          return;
        }
      }
      //proceed to B3 constraints
      else if (parameters != null && parameters.length > 0 && args != null && !isPertinentToApplicabilityCheckOnContainingCall(parent)) {
        final Set<ConstraintFormula> additionalConstraints = new LinkedHashSet<>();
        final HashSet<ConstraintFormula> ignoredConstraints = new HashSet<>();
        collectAdditionalConstraints(parameters, args, method, mySiteSubstitutor, additionalConstraints,
                                     ignoredConstraints, properties.isVarargs(), initialSubstitutor);

        proceedWithAdditionalConstraints(additionalConstraints, ignoredConstraints);
      }
    }

    resolveBounds(myInferenceVariables, initialSubstitutor);
  }

  private static boolean isPertinentToApplicabilityCheckOnContainingCall(@NotNull PsiElement parent) {
    return ThreadLocalTypes.hasBindingFor(parent);
  }

  private void collectAdditionalConstraints(PsiParameter[] parameters,
                                            PsiExpression[] args,
                                            PsiMethod parentMethod,
                                            PsiSubstitutor siteSubstitutor,
                                            Set<ConstraintFormula> additionalConstraints,
                                            Set<ConstraintFormula> ignoredConstraints,
                                            boolean varargs,
                                            PsiSubstitutor initialSubstitutor) {
    for (int i = 0; i < args.length; i++) {
      final PsiExpression arg = PsiUtil.skipParenthesizedExprDown(args[i]);
      if (arg != null) {
        final PsiSubstitutor nestedSubstitutor = myInferenceSessionContainer.findNestedSubstitutor(arg, myInferenceSubstitution);
        final PsiType parameterType = nestedSubstitutor.substitute(getParameterType(parameters, i, siteSubstitutor, varargs));
        if (parameterType == null) continue;
        ExpressionCompatibilityConstraint compatibilityConstraint = new ExpressionCompatibilityConstraint(arg, parameterType);
        if (arg instanceof PsiFunctionalExpression && ignoreLambdaConstraintTree(arg) || dependsOnIgnoredConstraint(ignoredConstraints, compatibilityConstraint)) {
          ignoredConstraints.add(compatibilityConstraint);
          continue;
        }
        if (!isPertinentToApplicability(arg, parentMethod)) {
          additionalConstraints.add(compatibilityConstraint);
        }
        additionalConstraints.add(new CheckedExceptionCompatibilityConstraint(arg, parameterType));
        if (arg instanceof PsiCallExpression) {
          //If the expression is a poly class instance creation expression (15.9) or a poly method invocation expression (15.12), 
          //the set contains all constraint formulas that would appear in the set C when determining the poly expression's invocation type.
          final JavaResolveResult resolveResult = PsiDiamondType.getDiamondsAwareResolveResult((PsiCallExpression)arg);
          final PsiMethod calledMethod = resolveResult instanceof MethodCandidateInfo ? (PsiMethod)resolveResult.getElement() : null;
          if (calledMethod != null && PsiPolyExpressionUtil.isMethodCallPolyExpression(arg, calledMethod)) {
            collectAdditionalConstraints(additionalConstraints, ignoredConstraints, (PsiCallExpression)arg, initialSubstitutor);
          }
        }
        else if (arg instanceof PsiLambdaExpression &&
                 isPertinentToApplicability(arg, parentMethod)) {
          collectLambdaReturnExpression(additionalConstraints, ignoredConstraints, (PsiLambdaExpression)arg,
                                        ((PsiLambdaExpression)arg).getGroundTargetType(parameterType),
                                        !isProperType(initialSubstitutor.substitute(parameterType)),
                                        initialSubstitutor);
        }
      }
    }
  }

  private boolean dependsOnIgnoredConstraint(Set<ConstraintFormula> ignoredConstraints, ExpressionCompatibilityConstraint compatibilityConstraint) {
    if (!ignoredConstraints.isEmpty()) {
      Set<InferenceVariable> inputVariables = compatibilityConstraint.getInputVariables(this);

      if (inputVariables != null) {
        for (ConstraintFormula ignoredConstraint : ignoredConstraints) {
          if (ignoredConstraint instanceof InputOutputConstraintFormula) {
            Set<InferenceVariable> inputsOfIgnored = ((InputOutputConstraintFormula)ignoredConstraint).getInputVariables(this);
            Set<InferenceVariable> outputVariables = ((InputOutputConstraintFormula)ignoredConstraint).getOutputVariables(inputsOfIgnored, this);
            if (outputVariables != null && ContainerUtil.intersects(outputVariables, inputVariables)) return true;
          }
        }
      }
    }
    return false;
  }
  
  public static boolean ignoreLambdaConstraintTree(PsiExpression arg) {
    for (PsiElement expr : MethodCandidateInfo.ourOverloadGuard.currentStack()) {
      if (PsiTreeUtil.getParentOfType(expr, PsiFunctionalExpression.class, false) == arg) {
        return true;
      }
    }
    return false;
  }

  private void collectLambdaReturnExpression(Set<ConstraintFormula> additionalConstraints,
                                             Set<ConstraintFormula> ignoredConstraints, 
                                             PsiLambdaExpression lambdaExpression,
                                             PsiType parameterType,
                                             boolean addConstraint,
                                             PsiSubstitutor initialSubstitutor) {
    final PsiType interfaceReturnType = LambdaUtil.getFunctionalInterfaceReturnType(parameterType);
    if (interfaceReturnType != null && (!PsiTypes.voidType().equals(interfaceReturnType) || !addConstraint)) {
      final List<PsiExpression> returnExpressions = LambdaUtil.getReturnExpressions(lambdaExpression);
      for (PsiExpression returnExpression : returnExpressions) {
        processReturnExpression(additionalConstraints, ignoredConstraints, returnExpression, interfaceReturnType, addConstraint, initialSubstitutor);
      }
    }
  }

  private void processReturnExpression(Set<ConstraintFormula> additionalConstraints,
                                       Set<ConstraintFormula> ignoredConstraints,
                                       PsiExpression returnExpression,
                                       @NotNull PsiType functionalType,
                                       boolean addConstraint,
                                       PsiSubstitutor initialSubstitutor) {
    if (returnExpression instanceof PsiCallExpression) {
      if (addConstraint) {
        final JavaResolveResult resolveResult = PsiDiamondType.getDiamondsAwareResolveResult((PsiCallExpression)returnExpression);
        if (resolveResult instanceof MethodCandidateInfo && 
            PsiPolyExpressionUtil.isMethodCallPolyExpression(returnExpression, ((MethodCandidateInfo)resolveResult).getElement())) {
          collectAdditionalConstraints(additionalConstraints, ignoredConstraints, (PsiCallExpression)returnExpression, initialSubstitutor);
        }
      }
      else {
        getInferenceSessionContainer().registerNestedSession(this, functionalType, returnExpression);
      }
    }
    else if (returnExpression instanceof PsiParenthesizedExpression) {
      processReturnExpression(additionalConstraints, ignoredConstraints, ((PsiParenthesizedExpression)returnExpression).getExpression(), functionalType, addConstraint, initialSubstitutor);
    }
    else if (returnExpression instanceof PsiConditionalExpression) {
      processReturnExpression(additionalConstraints, ignoredConstraints, ((PsiConditionalExpression)returnExpression).getThenExpression(), functionalType, addConstraint, initialSubstitutor);
      processReturnExpression(additionalConstraints, ignoredConstraints, ((PsiConditionalExpression)returnExpression).getElseExpression(), functionalType, addConstraint, initialSubstitutor);
    }
    else if (returnExpression instanceof PsiSwitchExpression) {
      for (PsiExpression resultExpression : PsiUtil.getSwitchResultExpressions((PsiSwitchExpression)returnExpression)) {
        processReturnExpression(additionalConstraints, ignoredConstraints, resultExpression, functionalType, addConstraint, initialSubstitutor);
      }
    }
    else if (returnExpression instanceof PsiLambdaExpression) {
      if (!ignoreLambdaConstraintTree(returnExpression)) {
        collectLambdaReturnExpression(additionalConstraints, ignoredConstraints, (PsiLambdaExpression)returnExpression, functionalType, myErased, initialSubstitutor);
      }
    }
  }

  private void collectAdditionalConstraints(final Set<ConstraintFormula> additionalConstraints,
                                            final Set<ConstraintFormula> ignoredConstraints, 
                                            final PsiCall callExpression,
                                            PsiSubstitutor initialSubstitutor) {
    PsiExpressionList argumentList = callExpression.getArgumentList();
    if (argumentList != null) {
      final JavaResolveResult result = PsiDiamondType.getDiamondsAwareResolveResult(callExpression);
      final PsiMethod method = result instanceof MethodCandidateInfo ? ((MethodCandidateInfo)result).getElement() :  null;
      if (method != null) {
        final PsiExpression[] newArgs = argumentList.getExpressions();
        final PsiParameter[] newParams = method.getParameterList().getParameters();
        if (newParams.length > 0) {
          collectAdditionalConstraints(newParams, newArgs, method, chooseSiteSubstitutor(null, result, method), additionalConstraints,
                                       ignoredConstraints, chooseVarargsMode(null, result), initialSubstitutor);
        }
      }
    }
  }

  public static PsiSubstitutor chooseSiteSubstitutor(MethodCandidateInfo currentMethod,
                                                     JavaResolveResult resolveResult, PsiMethod method) {
    return resolveResult instanceof MethodCandidateInfo && method != null && !method.isConstructor() //constructor reference was erased 
           ? ((MethodCandidateInfo)resolveResult).getSiteSubstitutor() 
           : currentMethod != null ? currentMethod.getSiteSubstitutor() : PsiSubstitutor.EMPTY;
  }


  public static boolean chooseVarargsMode(MethodCandidateInfo currentMethod,
                                          JavaResolveResult resolveResult) {
    return resolveResult instanceof MethodCandidateInfo && ((MethodCandidateInfo)resolveResult).isVarargs() ||
           currentMethod != null && currentMethod.isVarargs();
  }

  PsiSubstitutor getInstantiations(Collection<InferenceVariable> variables) {
    PsiSubstitutor substitutor = PsiSubstitutor.EMPTY;
    for (InferenceVariable variable : variables) {
      final PsiType equalsBound = getEqualsBound(variable, substitutor);
      if (!PsiTypes.nullType().equals(equalsBound)) {
        substitutor = substitutor.put(variable.getParameter(), equalsBound);
      }
    }
    return substitutor;
  }
  
  protected PsiSubstitutor prepareSubstitution() {
    boolean foundErrorMessage = false;
    Iterator<List<InferenceVariable>> iterator = InferenceVariablesOrder.resolveOrderIterator(myInferenceVariables, this);
    while (iterator.hasNext()) {
      final List<InferenceVariable> variables = iterator.next();
      for (InferenceVariable inferenceVariable : variables) {
        final PsiTypeParameter typeParameter = inferenceVariable.getParameter();
        PsiType instantiation = inferenceVariable.getInstantiation();
        //failed inference
        if (instantiation == PsiTypes.nullType()) {
          if (!foundErrorMessage) {
            foundErrorMessage = checkBoundsConsistency(mySiteSubstitutor, inferenceVariable) == PsiTypes.nullType();
          }
          mySiteSubstitutor = mySiteSubstitutor
            .put(typeParameter, JavaPsiFacade.getElementFactory(myManager.getProject()).createType(typeParameter));
        }
      }
    }
    return mySiteSubstitutor;
  }

  InitialInferenceState createInitialState(InferenceSessionContainer container,
                                           Collection<InitialInferenceState.VariableInfo> variables,
                                           PsiSubstitutor topInferenceSubstitutor) {
    return new InitialInferenceState(variables,
                                     topInferenceSubstitutor,
                                     myContext, 
                                     myInferenceSubstitution, 
                                     mySiteSubstitutor, 
                                     myIncorporationPhase.getCaptures(),
                                     myErased,
                                     container);
  }

  private void initBounds(PsiTypeParameter... typeParameters) {
    initBounds(myContext, typeParameters);
  }

  public InferenceVariable[] initBounds(PsiElement context, PsiTypeParameter... typeParameters) {
    return initBounds(context, mySiteSubstitutor, typeParameters);
  }

  /**
   * @param context context element
   * @param typeParameters type parameters belonging to the same class declaration
   * @return array of inference variables that correspond to the specified type parameters,
   * either exiting ones, or newly created ones.
   */
  public InferenceVariable[] initOrReuseVariables(PsiElement context,
                                                  PsiTypeParameter @NotNull ... typeParameters) {
    if (typeParameters.length == 0) return new InferenceVariable[0];
    Map<PsiTypeParameter, PsiType> map = myInferenceSubstitution.getSubstitutionMap();
    InferenceVariable[] variables = StreamEx.of(typeParameters).map(map::get)
      .map(PsiUtil::resolveClassInClassTypeOnly)
      .select(InferenceVariable.class)
      .toArray(new InferenceVariable[0]);
    if (variables.length != 0) {
      if (variables.length != typeParameters.length) {
        LOG.error("Unexpected: either all vars or no vars should be present");
      }
      return variables;
    }
    return initBounds(context, mySiteSubstitutor, typeParameters);
  }

  public InferenceVariable[] initBounds(PsiElement context,
                                        final PsiSubstitutor siteSubstitutor,
                                        PsiTypeParameter @NotNull ... typeParameters) {
    List<InferenceVariable> result = new ArrayList<>(typeParameters.length);
    for (PsiTypeParameter parameter : typeParameters) {
      String name = parameter.getName();
      if (myContext != null) {
        name += MathUtil.nonNegativeAbs(myContext.hashCode());
      }
      InferenceVariable variable = new InferenceVariable(context, parameter, name);
      result.add(variable);
      final PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(myManager.getProject());
      myInferenceSubstitution = myInferenceSubstitution.put(parameter, elementFactory.createType(variable));
      myRestoreNameSubstitution = myRestoreNameSubstitution.put(variable, elementFactory.createType(parameter));
      myInferenceVariables.add(variable);
    }
    for (InferenceVariable variable : result) {
