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

import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.core.JavaPsiBundle;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.JavaSdkVersion;
import com.intellij.openapi.projectRoots.JavaVersionService;
import com.intellij.openapi.roots.LanguageLevelProjectExtension;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.java.JavaFeature;
import com.intellij.pom.java.LanguageFeatureProvider;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.infos.ClassCandidateInfo;
import com.intellij.psi.infos.MethodCandidateInfo;
import com.intellij.psi.infos.MethodCandidateInfo.ApplicabilityLevel;
import com.intellij.psi.javadoc.PsiDocComment;
import com.intellij.psi.meta.PsiMetaData;
import com.intellij.psi.meta.PsiMetaOwner;
import com.intellij.psi.search.ProjectScope;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.ObjectUtils;
import com.intellij.util.ThreeState;
import com.intellij.util.TimeoutUtil;
import com.intellij.util.containers.CollectionFactory;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.JBIterable;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;

import java.util.*;
import java.util.function.Predicate;
import java.util.regex.Pattern;

import static com.intellij.java.syntax.parser.JavaKeywords.*;

public final class PsiUtil extends PsiUtilCore {
  private static final Logger LOG = Logger.getInstance(PsiUtil.class);

  public static final int ACCESS_LEVEL_PUBLIC = 4;
  public static final int ACCESS_LEVEL_PROTECTED = 3;
  public static final int ACCESS_LEVEL_PACKAGE_LOCAL = 2;
  public static final int ACCESS_LEVEL_PRIVATE = 1;
  public static final Key<Boolean> VALID_VOID_TYPE_IN_CODE_FRAGMENT = Key.create("VALID_VOID_TYPE_IN_CODE_FRAGMENT");

  private static final Pattern IGNORED_NAMES = Pattern.compile("ignored?[A-Za-z\\d]*");

  private static final @NotNull Map<CharSequence, JavaFeature> SOFT_KEYWORDS = CollectionFactory.createCharSequenceMap(true);

  static {
    SOFT_KEYWORDS.put(EXPORTS, JavaFeature.MODULES);
    SOFT_KEYWORDS.put(MODULE, JavaFeature.MODULES);
    SOFT_KEYWORDS.put(OPEN, JavaFeature.MODULES);
    SOFT_KEYWORDS.put(OPENS, JavaFeature.MODULES);
    SOFT_KEYWORDS.put(PERMITS, JavaFeature.SEALED_CLASSES);
    SOFT_KEYWORDS.put(PROVIDES, JavaFeature.MODULES);
    SOFT_KEYWORDS.put(RECORD, JavaFeature.RECORDS);
    SOFT_KEYWORDS.put(REQUIRES, JavaFeature.MODULES);
    SOFT_KEYWORDS.put(SEALED, JavaFeature.SEALED_CLASSES);
    SOFT_KEYWORDS.put(TO, JavaFeature.MODULES);
    SOFT_KEYWORDS.put(TRANSITIVE, JavaFeature.MODULES);
    SOFT_KEYWORDS.put(USES, JavaFeature.MODULES);
    SOFT_KEYWORDS.put(VAR, JavaFeature.LVTI);
    SOFT_KEYWORDS.put(VALUE, JavaFeature.VALHALLA_VALUE_CLASSES);
    SOFT_KEYWORDS.put(WHEN, JavaFeature.PATTERN_GUARDS_AND_RECORD_PATTERNS);
    SOFT_KEYWORDS.put(WITH, JavaFeature.MODULES);
    SOFT_KEYWORDS.put(YIELD, JavaFeature.SWITCH_EXPRESSION);
  }

  private PsiUtil() {}

  public static boolean isOnAssignmentLeftHand(@NotNull PsiExpression expr) {
    PsiElement parent = PsiTreeUtil.skipParentsOfType(expr, PsiParenthesizedExpression.class);
    return parent instanceof PsiAssignmentExpression &&
           PsiTreeUtil.isAncestor(((PsiAssignmentExpression)parent).getLExpression(), expr, false);
  }

  public static boolean isAccessibleFromPackage(@NotNull PsiModifierListOwner element, @NotNull PsiPackage aPackage) {
    if (element.hasModifierProperty(PsiModifier.PUBLIC)) return true;
    return !element.hasModifierProperty(PsiModifier.PRIVATE) &&
           JavaPsiFacade.getInstance(element.getProject()).isInPackage(element, aPackage);
  }

  public static boolean isAccessedForWriting(@NotNull PsiExpression expr) {
    if (isOnAssignmentLeftHand(expr)) return true;
    PsiElement parent = skipParenthesizedExprUp(expr.getParent());
    return isIncrementDecrementOperation(parent);
  }

  public static boolean isAccessedForReading(@NotNull PsiExpression expr) {
    PsiElement parent = PsiTreeUtil.skipParentsOfType(expr, PsiParenthesizedExpression.class);
    return !(parent instanceof PsiAssignmentExpression) ||
           !PsiTreeUtil.isAncestor(((PsiAssignmentExpression)parent).getLExpression(), expr, false) ||
           ((PsiAssignmentExpression)parent).getOperationTokenType() != JavaTokenType.EQ;
  }

  public static boolean isAccessible(@NotNull PsiMember member, @NotNull PsiElement place, @Nullable PsiClass accessObjectClass) {
    return isAccessible(place.getProject(), member, place, accessObjectClass);
  }

  /**
   * Checks that a *resolved* to {@code member} reference at {@code place} is accessible: it's visible by visibility rules as well as
   * by module (java) rules.
   *
   * <p>NOTE:</p>
   * If there is no module (IDEA's) dependency from module with {@code place} on a module with {@code member},
   * then reference won't be resolved and this method will return {@code true}.
   * <p>
   * Please use {@link #isMemberAccessibleAt(PsiMember, PsiElement)} to catch these cases as well
   */
  public static boolean isAccessible(@NotNull Project project, @NotNull PsiMember member,
                                     @NotNull PsiElement place, @Nullable PsiClass accessObjectClass) {
    return JavaPsiFacade.getInstance(project).getResolveHelper().isAccessible(member, place, accessObjectClass);
  }

  /**
   * Checks that reference on {@code member} inserted at {@code place} will be resolved and accessible
   */
  public static boolean isMemberAccessibleAt(@NotNull PsiMember member, @NotNull PsiElement place) {
    VirtualFile virtualFile = getVirtualFile(member);
    return (virtualFile == null || place.getResolveScope().contains(virtualFile)) && isAccessible(member, place, null);
  }

  public static @NotNull JavaResolveResult getAccessObjectClass(@NotNull PsiExpression expression) {
    if (expression instanceof PsiSuperExpression) return JavaResolveResult.EMPTY;
    PsiType type = expression.getType();
    JavaResolveResult accessObject = getAccessObjectClass(type, expression);
    if (accessObject != null) return accessObject;

    if (type == null && expression instanceof PsiReferenceExpression) {
      JavaResolveResult resolveResult = ((PsiReferenceExpression)expression).advancedResolve(false);
      if (resolveResult.getElement() instanceof PsiClass) {
        return resolveResult;
      }
    }
    return JavaResolveResult.EMPTY;
  }

  private static @Nullable JavaResolveResult getAccessObjectClass(@Nullable PsiType type, @NotNull PsiElement place) {
    if (type instanceof PsiClassType) {
      return ((PsiClassType)type).resolveGenerics();
    }
    if (type instanceof PsiDisjunctionType) {
      PsiType lub = ((PsiDisjunctionType)type).getLeastUpperBound();
      if (lub instanceof PsiClassType) {
        return ((PsiClassType)lub).resolveGenerics();
      }
    }

    if (type instanceof PsiCapturedWildcardType) {
      PsiType upperBound = ((PsiCapturedWildcardType)type).getUpperBound();
      if (upperBound instanceof PsiClassType) {
        PsiClass resolved = ((PsiClassType)upperBound).resolve();
        PsiFile containingFile = resolved != null ? resolved.getContainingFile() : null;
        String packageName = containingFile instanceof PsiClassOwner ? ((PsiClassOwner)containingFile).getPackageName() : null;
        String classText = StringUtil.isEmptyOrSpaces(packageName) ? "" : "package " + packageName + ";\n ";
        classText += "class I<T extends " + upperBound.getCanonicalText() + "> {}";
        PsiJavaFile file =
          (PsiJavaFile)PsiFileFactory.getInstance(place.getProject()).createFileFromText("inference_dummy.java", JavaLanguage.INSTANCE, classText);
        PsiTypeParameter freshParameter = file.getClasses()[0].getTypeParameters()[0];
        return new ClassCandidateInfo(freshParameter, PsiSubstitutor.EMPTY);
      }
    }

    if (type instanceof PsiArrayType) {
      return getAccessObjectClass(((PsiArrayType)type).getComponentType(), place);
    }
    return null;
  }

  public static boolean isConstantExpression(@Nullable PsiExpression expression) {
    return expression != null && JavaPsiFacade.getInstance(expression.getProject()).isConstantExpression(expression);
  }

  // todo: move to PsiThrowsList?
  public static void addException(@NotNull PsiMethod method, @NotNull String exceptionFQName) throws IncorrectOperationException {
    PsiClass exceptionClass = JavaPsiFacade.getInstance(method.getProject()).findClass(exceptionFQName, method.getResolveScope());
    addException(method, exceptionClass, exceptionFQName);
  }

  public static void addException(@NotNull PsiMethod method, @NotNull PsiClass exceptionClass) throws IncorrectOperationException {
    addException(method, exceptionClass, exceptionClass instanceof PsiTypeParameter ? exceptionClass.getName() : exceptionClass.getQualifiedName());
  }

  private static void addException(@NotNull PsiMethod method,
                                   @Nullable PsiClass exceptionClass,
                                   @Nullable String exceptionName) throws IncorrectOperationException {
    if (exceptionClass == null && exceptionName == null) {
      throw new IllegalArgumentException("One of exceptionName, exceptionClass must be not null");
    }
    PsiReferenceList throwsList = method.getThrowsList();
    PsiJavaCodeReferenceElement[] refs = throwsList.getReferenceElements();
    boolean replaced = false;
    for (PsiJavaCodeReferenceElement ref : refs) {
      if (exceptionClass != null && ref.isReferenceTo(exceptionClass)) return;
      PsiClass aClass = (PsiClass)ref.resolve();
      if (exceptionClass == null || aClass == null) {
        continue;
      }
      if (aClass.isInheritor(exceptionClass, true)) {
        if (replaced) {
          ref.delete();
        }
        else {
          PsiElementFactory factory = JavaPsiFacade.getElementFactory(method.getProject());
          PsiJavaCodeReferenceElement ref1;
          if (exceptionName != null) {
            ref1 = factory.createReferenceElementByFQClassName(exceptionName, method.getResolveScope());
          }
          else {
            PsiClassType type = factory.createType(exceptionClass);
            ref1 = factory.createReferenceElementByType(type);
          }
          ref.replace(ref1);
          replaced = true;
        }
      }
      else if (exceptionClass.isInheritor(aClass, true)) {
        return;
      }
    }
    if (replaced) return;

    PsiElementFactory factory = JavaPsiFacade.getElementFactory(method.getProject());
    PsiJavaCodeReferenceElement ref;
    if (exceptionName != null) {
      ref = factory.createReferenceElementByFQClassName(exceptionName, method.getResolveScope());
    }
    else {
      PsiClass superClass = exceptionClass.getSuperClass();
      while (superClass != null && isLocalOrAnonymousClass(superClass)) {
        superClass = superClass.getSuperClass();
      }
      PsiClassType type = factory.createType(superClass != null ? superClass : exceptionClass);
      ref = factory.createReferenceElementByType(type);
    }
    throwsList.add(ref);
  }

  // todo: move to PsiThrowsList?
  public static void removeException(@NotNull PsiMethod method, String exceptionClass) throws IncorrectOperationException {
    PsiJavaCodeReferenceElement[] refs = method.getThrowsList().getReferenceElements();
    for (PsiJavaCodeReferenceElement ref : refs) {
      if (ref.getCanonicalText().equals(exceptionClass)) {
        ref.delete();
      }
    }
  }

  public static boolean isVariableNameUnique(@NotNull String name, @NotNull PsiElement place) {
    PsiResolveHelper helper = JavaPsiFacade.getInstance(place.getProject()).getResolveHelper();
    return helper.resolveAccessibleReferencedVariable(name, place) == null;
  }

  /**
   * @return enclosing the outermost (method or class initializer) body but not higher than scope
   */
  public static @Nullable PsiElement getTopLevelEnclosingCodeBlock(@Nullable PsiElement element, PsiElement scope) {
    PsiElement blockSoFar = null;
    while (element != null) {
      // variable can be defined in for loop initializer
      PsiElement parent = element.getParent();
      if (!(parent instanceof PsiExpression) || parent instanceof PsiLambdaExpression) {
        if (element instanceof PsiCodeBlock || element instanceof PsiForStatement || element instanceof PsiForeachStatement) {
          blockSoFar = element;
        }

        if (parent instanceof PsiMethod
            && parent.getParent() instanceof PsiClass
            && !isLocalOrAnonymousClass((PsiClass)parent.getParent()))
          break;
        if (parent instanceof PsiClassInitializer && !(parent.getParent() instanceof PsiAnonymousClass)) break;
        if (parent instanceof PsiField && ((PsiField) parent).getInitializer() == element) {
          blockSoFar = element;
        }
        if (parent instanceof PsiClassLevelDeclarationStatement) {
          parent = parent.getParent();
        }
        if (element instanceof PsiClass && !isLocalOrAnonymousClass((PsiClass)element)) {
          break;
        }
        if (element instanceof PsiFile && getTemplateLanguageFile(element) != null) {
          return element;
        }
      }
      if (element == scope) break;
      element = parent;
    }
    return blockSoFar;
  }

  public static boolean isLocalOrAnonymousClass(@NotNull PsiClass psiClass) {
    return psiClass instanceof PsiAnonymousClass || isLocalClass(psiClass);
  }

  public static boolean isLocalClass(@NotNull PsiClass psiClass) {
    PsiElement parent = psiClass.getParent();
    if (parent instanceof PsiDeclarationStatement && parent.getParent() instanceof PsiCodeBlock) {
      return true;
    }

    if (parent instanceof PsiClass) {
      return isLocalOrAnonymousClass((PsiClass)parent);
    }
    return false;
  }

  public static boolean isAbstractClass(@NotNull PsiClass clazz) {
    PsiModifierList modifierList = clazz.getModifierList();
    return modifierList != null && modifierList.hasModifierProperty(PsiModifier.ABSTRACT);
  }

  /**
   * @return topmost code block where variable makes sense
   */
  @Contract(pure = true)
  public static @Nullable PsiElement getVariableCodeBlock(@NotNull PsiVariable variable, @Nullable PsiElement context) {
    PsiElement codeBlock = null;
    if (variable instanceof PsiParameter) {
      PsiElement declarationScope = ((PsiParameter)variable).getDeclarationScope();
      if (variable instanceof PsiPatternVariable) {
        return declarationScope;
      }
      if (declarationScope instanceof PsiCatchSection) {
        codeBlock = ((PsiCatchSection)declarationScope).getCatchBlock();
      }
      else if (declarationScope instanceof PsiForeachStatement) {
        codeBlock = ((PsiForeachStatement)declarationScope).getBody();
      }
      else if (declarationScope instanceof PsiMethod) {
        codeBlock = ((PsiMethod)declarationScope).getBody();
      } else if (declarationScope instanceof PsiLambdaExpression) {
        codeBlock = ((PsiLambdaExpression)declarationScope).getBody();
      }
      else if (declarationScope instanceof PsiCodeBlock) {
        codeBlock = declarationScope;
      }
    }
    else if (variable instanceof PsiResourceVariable) {
      PsiElement resourceList = variable.getParent();
      return resourceList != null ? resourceList.getParent() : null;  // use try statement as topmost
    }
    else if (variable instanceof PsiLocalVariable && variable.getParent() instanceof PsiForStatement) {
      return variable.getParent();
    }
    else if (variable instanceof PsiField && context != null) {
      PsiClass aClass = ((PsiField) variable).getContainingClass();
      while (context != null && context.getParent() != aClass) {
        context = context.getParent();
        if (context instanceof PsiClassLevelDeclarationStatement) return null;
      }
      return context instanceof PsiMethod ?
             ((PsiMethod) context).getBody() :
             context instanceof PsiClassInitializer ? ((PsiClassInitializer) context).getBody() : null;
    }
    else {
      PsiElement scope = variable.getParent() == null ? null : variable.getParent().getParent();
      codeBlock = getTopLevelEnclosingCodeBlock(variable, scope);
      if (codeBlock != null && codeBlock.getParent() instanceof PsiSwitchStatement) codeBlock = codeBlock.getParent().getParent();
      if (codeBlock == null && scope instanceof PsiCodeBlock && scope.getParent() instanceof PsiSwitchExpression) {
        codeBlock = scope;
      }
    }
    return codeBlock;
  }

  @Contract("null -> false")
  public static boolean isIncrementDecrementOperation(@Nullable PsiElement element) {
    if (element instanceof PsiUnaryExpression) {
      IElementType sign = ((PsiUnaryExpression)element).getOperationTokenType();
      return sign == JavaTokenType.PLUSPLUS || sign == JavaTokenType.MINUSMINUS;
    }
    return false;
  }

  public static @Unmodifiable @NotNull List<PsiExpression> getSwitchResultExpressions(@NotNull PsiSwitchExpression switchExpression) {
    PsiCodeBlock body = switchExpression.getBody();
    if (body != null) {
      List<PsiExpression> result = new ArrayList<>();
      PsiStatement[] statements = body.getStatements();
      for (PsiStatement statement : statements) {
        if (statement instanceof PsiSwitchLabeledRuleStatement) {
          PsiStatement ruleBody = ((PsiSwitchLabeledRuleStatement)statement).getBody();
          if (ruleBody instanceof PsiExpressionStatement) {
            result.add(((PsiExpressionStatement)ruleBody).getExpression());
          }
          else if (ruleBody instanceof PsiBlockStatement) {
            collectSwitchResultExpressions(result, ruleBody);
          }
        }
        else {
           collectSwitchResultExpressions(result, statement);
        }
      }
      return result;
    }
    return Collections.emptyList();
  }

  private static void collectSwitchResultExpressions(@NotNull List<? super PsiExpression> result, @NotNull PsiElement container) {
    List<PsiYieldStatement> yields = new ArrayList<>();
    addStatements(yields, container, PsiYieldStatement.class, element -> element instanceof PsiSwitchExpression);
    for (PsiYieldStatement statement : yields) {
      ContainerUtil.addIfNotNull(result, statement.getExpression());
    }
  }

  /**
   * @param typeCast cast to check
   * @return true if the supplied cast is necessary to fulfill the signature polymorphic method contract
   * @see <a href="https://docs.oracle.com/en/java/javase/21/docs//api/java.base/java/lang/invoke/MethodHandle.html#sigpoly">Signature polymorphism</a> in Java API reference
   */
  // see http://download.java.net/jdk7/docs/api/java/lang/invoke/MethodHandle.html#sigpoly
  public static boolean isInSignaturePolymorphicCall(@NotNull PsiTypeCastExpression typeCast) {
    if (!isLanguageLevel7OrHigher(typeCast)) return false;

    // return type
    final PsiExpression operand = typeCast.getOperand();
    if (operand instanceof PsiMethodCallExpression && isPolymorphicMethod((PsiMethodCallExpression)operand)) {
      return true;
    }

    // argument type
    final PsiElement exprList = skipParenthesizedExprUp(typeCast.getParent());
    return exprList instanceof PsiExpressionList &&
           exprList.getParent() instanceof PsiMethodCallExpression &&
           isPolymorphicMethod((PsiMethodCallExpression)exprList.getParent());
  }

  private static boolean isPolymorphicMethod(PsiMethodCallExpression expression) {
    PsiElement target = expression.getMethodExpression().resolve();
    return target instanceof PsiMethod &&
           AnnotationUtil.isAnnotated((PsiMethod)target, CommonClassNames.JAVA_LANG_INVOKE_MH_POLYMORPHIC, 0);
  }

  @MagicConstant(intValues = {ACCESS_LEVEL_PUBLIC, ACCESS_LEVEL_PROTECTED, ACCESS_LEVEL_PACKAGE_LOCAL, ACCESS_LEVEL_PRIVATE})
  public @interface AccessLevel {}

  @AccessLevel
  public static int getAccessLevel(@NotNull PsiModifierList modifierList) {
    if (modifierList.hasModifierProperty(PsiModifier.PRIVATE)) {
      return ACCESS_LEVEL_PRIVATE;
    }
    if (modifierList.hasModifierProperty(PsiModifier.PACKAGE_LOCAL)) {
      return ACCESS_LEVEL_PACKAGE_LOCAL;
    }
    if (modifierList.hasModifierProperty(PsiModifier.PROTECTED)) {
      return ACCESS_LEVEL_PROTECTED;
    }
    return ACCESS_LEVEL_PUBLIC;
  }

  /**
   * @see com.intellij.lang.jvm.util.JvmUtil#getAccessModifier(int) for JVM language analogue.
   */
  @PsiModifier.ModifierConstant
  public static @NotNull String getAccessModifier(@AccessLevel int accessLevel) {
    if (accessLevel <= 0 || accessLevel > accessModifiers.length) {
      throw new IllegalArgumentException("Unknown level:" + accessLevel);
    }
    @PsiModifier.ModifierConstant String modifier =  accessModifiers[accessLevel - 1];
    return modifier;
  }

  private static final @PsiModifier.ModifierConstant String[] accessModifiers = {
    PsiModifier.PRIVATE, PsiModifier.PACKAGE_LOCAL, PsiModifier.PROTECTED, PsiModifier.PUBLIC
  };

  /**
   * @return true if the element specified is a statement or expression statement. see JLS 14.5-14.8
   */
  public static boolean isStatement(@NotNull PsiElement element) {
    PsiElement parent = element.getParent();

    if (element instanceof PsiExpressionListStatement) {
      // statement list allowed in for() init or update only
      if (!(parent instanceof PsiForStatement)) return false;
      PsiForStatement forStatement = (PsiForStatement)parent;
      if (!(element == forStatement.getInitialization() || element == forStatement.getUpdate())) return false;
      PsiExpressionList expressionList = ((PsiExpressionListStatement)element).getExpressionList();
      for (PsiExpression expression : expressionList.getExpressions()) {
        if (!isStatement(expression)) return false;
      }
      return true;
    }

    if (element instanceof PsiExpressionStatement) {
      return parent instanceof PsiSwitchLabeledRuleStatement && ((PsiSwitchLabeledRuleStatement)parent).getEnclosingSwitchBlock() instanceof PsiSwitchExpression ||
             isStatement(((PsiExpressionStatement)element).getExpression());
    }

    if (element instanceof PsiDeclarationStatement) {
      if (parent instanceof PsiCodeBlock) return true;
      if (parent instanceof PsiCodeFragment) return true;
      if (!(parent instanceof PsiForStatement) || ((PsiForStatement)parent).getBody() == element) {
        return false;
      }
    }

    if (element instanceof PsiStatement) return true;
    if (element instanceof PsiAssignmentExpression) return true;
    if (isIncrementDecrementOperation(element)) return true;
    if (element instanceof PsiMethodCallExpression) return true;
    if (element instanceof PsiNewExpression) {
      return !(((PsiNewExpression)element).getType() instanceof PsiArrayType);
    }

    return element instanceof PsiCodeBlock;
  }

  public static @Nullable PsiElement getEnclosingStatement(PsiElement element) {
    while (element != null) {
      if (element.getParent() instanceof PsiCodeBlock) return element;
      element = element.getParent();
    }
    return null;
  }


  public static @Nullable PsiElement getElementInclusiveRange(@NotNull PsiElement scope, @NotNull TextRange range) {
    PsiElement psiElement = scope.findElementAt(range.getStartOffset());
    while (psiElement != null && !psiElement.getTextRange().contains(range)) {
      if (psiElement == scope) return null;
      psiElement = psiElement.getParent();
    }
    return psiElement;
  }

  public static @Nullable PsiClass resolveClassInType(@Nullable PsiType type) {
    if (type instanceof PsiClassType) {
      return ((PsiClassType) type).resolve();
    }
    if (type instanceof PsiArrayType) {
      return resolveClassInType(type.getDeepComponentType());
    }
    if (type instanceof PsiDisjunctionType) {
      PsiType lub = ((PsiDisjunctionType)type).getLeastUpperBound();
      if (lub instanceof PsiClassType) {
        return ((PsiClassType)lub).resolve();
      }
    }
    return null;
  }

  public static @Nullable PsiClass resolveClassInClassTypeOnly(@Nullable PsiType type) {
    return type instanceof PsiClassType ? ((PsiClassType)type).resolve() : null;
  }

  public static @NotNull PsiClassType.ClassResolveResult resolveGenericsClassInType(@Nullable PsiType type) {
    if (type instanceof PsiClassType) {
      PsiClassType classType = (PsiClassType) type;
      return classType.resolveGenerics();
    }
    if (type instanceof PsiArrayType) {
      return resolveGenericsClassInType(((PsiArrayType) type).getComponentType());
    }
    if (type instanceof PsiDisjunctionType) {
      PsiType lub = ((PsiDisjunctionType)type).getLeastUpperBound();
      if (lub instanceof PsiClassType) {
        return ((PsiClassType)lub).resolveGenerics();
      }
    }
    return PsiClassType.ClassResolveResult.EMPTY;
  }

  public static @NotNull PsiType convertAnonymousToBaseType(@NotNull PsiType type) {
    PsiClass psiClass = resolveClassInType(type);
    if (psiClass instanceof PsiAnonymousClass) {
      type = PsiTypesUtil.createArrayType(((PsiAnonymousClass) psiClass).getBaseClassType(), type.getArrayDimensions());
    }
    return type;
  }

  public static boolean isApplicable(@NotNull PsiMethod method,
                                     @NotNull PsiSubstitutor substitutorForMethod,
                                     @NotNull PsiExpressionList argList) {
    return getApplicabilityLevel(method, substitutorForMethod, argList) != ApplicabilityLevel.NOT_APPLICABLE;
  }

  public static boolean isApplicable(@NotNull PsiMethod method,
                                     @NotNull PsiSubstitutor substitutorForMethod,
                                     PsiExpression @NotNull [] argList) {
    PsiType[] types = ContainerUtil.map2Array(argList, PsiType.class, PsiExpression.EXPRESSION_TO_TYPE);
    return getApplicabilityLevel(method, substitutorForMethod, types, getLanguageLevel(method)) != ApplicabilityLevel.NOT_APPLICABLE;
  }

  @MethodCandidateInfo.ApplicabilityLevelConstant
  public static int getApplicabilityLevel(@NotNull PsiMethod method,
                                          @NotNull PsiSubstitutor substitutorForMethod,
                                          @NotNull PsiExpressionList argList) {
    return getApplicabilityLevel(method, substitutorForMethod, argList.getExpressionTypes(), getLanguageLevel(argList));
  }

  @MethodCandidateInfo.ApplicabilityLevelConstant
  public static int getApplicabilityLevel(@NotNull PsiMethod method,
                                          @NotNull PsiSubstitutor substitutorForMethod,
                                          PsiType @NotNull [] args,
                                          @NotNull LanguageLevel languageLevel) {
    return getApplicabilityLevel(method, substitutorForMethod, args, languageLevel,
                                 true, true, ApplicabilityChecker.ASSIGNABILITY_CHECKER);
  }


  @FunctionalInterface
  public interface ApplicabilityChecker {
    ApplicabilityChecker ASSIGNABILITY_CHECKER =
      (left, right, allowUncheckedConversion, argId) -> TypeConversionUtil.isAssignable(left, right, allowUncheckedConversion);

    boolean isApplicable(PsiType left, PsiType right, boolean allowUncheckedConversion, int argId);
  }

  @MethodCandidateInfo.ApplicabilityLevelConstant
  public static int getApplicabilityLevel(@NotNull PsiMethod method,
                                          @NotNull PsiSubstitutor substitutorForMethod,
                                          PsiType @NotNull [] args,
                                          @NotNull LanguageLevel languageLevel,
                                          boolean allowUncheckedConversion,
                                          boolean checkVarargs,
                                          @NotNull ApplicabilityChecker function) {
    PsiParameter[] parameters = method.getParameterList().getParameters();
    if (args.length < parameters.length - 1) return ApplicabilityLevel.NOT_APPLICABLE;

    PsiClass containingClass = method.getContainingClass();
    boolean isRaw = containingClass != null && isRawSubstitutor(method, substitutorForMethod) && isRawSubstitutor(containingClass, substitutorForMethod);
    if (!areFirstArgumentsApplicable(args, parameters, languageLevel, substitutorForMethod, isRaw, allowUncheckedConversion, function)) {
      return ApplicabilityLevel.NOT_APPLICABLE;
    }
    if (args.length == parameters.length) {
      if (parameters.length == 0) return ApplicabilityLevel.FIXED_ARITY;
      PsiType parmType = getParameterType(parameters[parameters.length - 1], languageLevel, substitutorForMethod);
      PsiType argType = args[args.length - 1];
      if (argType == null) return ApplicabilityLevel.NOT_APPLICABLE;
      if (function.isApplicable(parmType, argType, allowUncheckedConversion, parameters.length - 1)) return ApplicabilityLevel.FIXED_ARITY;

      if (isRaw) {
        PsiType erasedParamType = TypeConversionUtil.erasure(parmType);
        if (erasedParamType != null && function.isApplicable(erasedParamType, argType, allowUncheckedConversion, parameters.length - 1)) {
          return ApplicabilityLevel.FIXED_ARITY;
        }
      }
    }

    if (checkVarargs && method.isVarArgs() && languageLevel.compareTo(LanguageLevel.JDK_1_5) >= 0) {
      if (args.length < parameters.length) return ApplicabilityLevel.VARARGS;
      PsiParameter lastParameter = parameters.length == 0 ? null : parameters[parameters.length - 1];
      if (lastParameter == null || !lastParameter.isVarArgs()) return ApplicabilityLevel.NOT_APPLICABLE;
      PsiType lastParmType = getParameterType(lastParameter, languageLevel, substitutorForMethod);
      if (!(lastParmType instanceof PsiArrayType)) return ApplicabilityLevel.NOT_APPLICABLE;
      lastParmType = ((PsiArrayType)lastParmType).getComponentType();
      if (lastParmType instanceof PsiCapturedWildcardType &&
          !JavaVersionService.getInstance().isAtLeast(((PsiCapturedWildcardType)lastParmType).getContext(), JavaSdkVersion.JDK_1_8)) {
        lastParmType = ((PsiCapturedWildcardType)lastParmType).getWildcard();
      }
      if (lastParmType instanceof PsiClassType) {
        lastParmType = ((PsiClassType)lastParmType).setLanguageLevel(languageLevel);
      }
      for (int i = parameters.length - 1; i < args.length; i++) {
        PsiType argType = args[i];
        if (argType == null || !function.isApplicable(lastParmType, argType, allowUncheckedConversion, i)) {
          return ApplicabilityLevel.NOT_APPLICABLE;
        }
      }
      return ApplicabilityLevel.VARARGS;
    }

    return ApplicabilityLevel.NOT_APPLICABLE;
  }

  private static boolean areFirstArgumentsApplicable(PsiType @NotNull [] args,
                                                     PsiParameter @NotNull [] parameters,
                                                     @NotNull LanguageLevel languageLevel,
                                                     @NotNull PsiSubstitutor substitutorForMethod,
                                                     boolean isRaw,
                                                     boolean allowUncheckedConversion, ApplicabilityChecker function) {
    for (int i = 0; i < parameters.length - 1; i++) {
      PsiType type = args[i];
      if (type == null) return false;
      PsiParameter parameter = parameters[i];
      PsiType substitutedParmType = getParameterType(parameter, languageLevel, substitutorForMethod);
      if (isRaw) {
        PsiType substErasure = TypeConversionUtil.erasure(substitutedParmType);
        if (substErasure != null && !function.isApplicable(substErasure, type, allowUncheckedConversion, i)) {
          return false;
        }
      }
