// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.groovy.lang.psi.util;

import com.intellij.lang.ASTNode;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.impl.light.JavaIdentifier;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.util.*;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.SmartList;
import com.intellij.util.VisibilityUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.Stack;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import org.jetbrains.annotations.*;
import org.jetbrains.plugins.groovy.codeInspection.utils.ControlFlowUtils;
import org.jetbrains.plugins.groovy.config.GroovyConfigUtils;
import org.jetbrains.plugins.groovy.ext.newify.NewifyMemberContributor;
import org.jetbrains.plugins.groovy.extensions.GroovyApplicabilityProvider;
import org.jetbrains.plugins.groovy.lang.lexer.GroovyLexer;
import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
import org.jetbrains.plugins.groovy.lang.lexer.TokenSets;
import org.jetbrains.plugins.groovy.lang.psi.*;
import org.jetbrains.plugins.groovy.lang.psi.api.EmptyGroovyResolveResult;
import org.jetbrains.plugins.groovy.lang.psi.api.GroovyMethodResult;
import org.jetbrains.plugins.groovy.lang.psi.api.GroovyResolveResult;
import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.GrListOrMap;
import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.GrModifierList;
import org.jetbrains.plugins.groovy.lang.psi.api.signatures.GrSignature;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.*;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentLabel;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrNamedArgument;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrSpreadArgument;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrCodeBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrOpenBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.branch.GrAssertStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.branch.GrReturnStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.branch.GrThrowStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.clauses.GrCaseSection;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.clauses.GrForInClause;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.clauses.GrTraditionalForClause;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.*;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrIndexProperty;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrMethodCallExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrPropertySelection;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameter;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrAnonymousClassDefinition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrAccessorMethod;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrEnumConstant;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrGdkMethod;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod;
import org.jetbrains.plugins.groovy.lang.psi.api.toplevel.imports.GrImportStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.util.GrNamedArgumentsOwner;
import org.jetbrains.plugins.groovy.lang.psi.controlFlow.Instruction;
import org.jetbrains.plugins.groovy.lang.psi.dataFlow.types.TypeInferenceHelper;
import org.jetbrains.plugins.groovy.lang.psi.impl.GrAnnotationUtil;
import org.jetbrains.plugins.groovy.lang.psi.impl.GrMapType;
import org.jetbrains.plugins.groovy.lang.psi.impl.GrTupleType;
import org.jetbrains.plugins.groovy.lang.psi.impl.PsiImplUtil;
import org.jetbrains.plugins.groovy.lang.psi.impl.signatures.GrClosureSignatureUtil;
import org.jetbrains.plugins.groovy.lang.psi.impl.statements.expressions.TypesUtil;
import org.jetbrains.plugins.groovy.lang.psi.impl.statements.expressions.literals.GrLiteralImpl;
import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrBindingVariable;
import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GroovyScriptClass;
import org.jetbrains.plugins.groovy.lang.psi.typeEnhancers.ClosureParameterEnhancer;
import org.jetbrains.plugins.groovy.lang.resolve.ResolveUtil;
import org.jetbrains.plugins.groovy.lang.resolve.api.*;
import org.jetbrains.plugins.groovy.lang.resolve.ast.ConstructorAnnotationsProcessor;
import org.jetbrains.plugins.groovy.lang.resolve.impl.AccessibilityKt;
import org.jetbrains.plugins.groovy.lang.resolve.impl.ArgumentsKt;
import org.jetbrains.plugins.groovy.lang.resolve.impl.NullArgumentMapping;
import org.jetbrains.plugins.groovy.lang.resolve.processors.MethodResolverProcessor;
import org.jetbrains.plugins.groovy.lang.typing.GroovyClosureType;
import org.jetbrains.plugins.groovy.util.dynamicMembers.DynamicMemberUtils;

import java.util.*;

import static com.intellij.psi.CommonClassNames.*;
import static com.intellij.psi.GenericsUtil.isTypeArgumentsApplicable;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.jetbrains.plugins.groovy.lang.psi.util.GroovyCommonClassNames.*;

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

  public static final Key<JavaIdentifier> NAME_IDENTIFIER = new Key<>("Java Identifier");
  public static final @NonNls Set<String> OPERATOR_METHOD_NAMES = Set.of(
    "plus", "minus", "multiply", "power", "div", "mod", "or", "and", "xor", "next", "previous", "getAt", "putAt", "leftShift", "rightShift",
    "isCase", "bitwiseNegate", "negative", "positive", "call"
  );

  private PsiUtil() {
  }

  public static @Nullable String getMethodName(GrMethodCall methodCall) {
    GrExpression invokedExpression = methodCall.getInvokedExpression();
    if (!(invokedExpression instanceof GrReferenceExpression)) return null;

    return ((GrReferenceExpression)invokedExpression).getReferenceName();
  }

  public static @Nullable String getUnqualifiedMethodName(GrMethodCall methodCall) {
    GrExpression invokedExpression = methodCall.getInvokedExpression();
    if (!(invokedExpression instanceof GrReferenceExpression)) return null;

    if (((GrReferenceExpression)invokedExpression).isQualified()) return null;

    return ((GrReferenceExpression)invokedExpression).getReferenceName();
  }

  public static boolean isLValue(GroovyPsiElement element) {
    if (element instanceof GrExpression) {
      PsiElement parent = PsiTreeUtil.skipParentsOfType(element, GrParenthesizedExpression.class);
      if (parent instanceof GrTuple) return true;
      return parent instanceof GrAssignmentExpression &&
             PsiTreeUtil.isAncestor(((GrAssignmentExpression)parent).getLValue(), element, false);
    }
    return false;
  }

  public static boolean isApplicable(PsiType @Nullable [] argumentTypes,
                                     @NotNull PsiMethod method,
                                     PsiSubstitutor substitutor,
                                     PsiElement place,
                                     final boolean eraseParameterTypes) {
    return isApplicableConcrete(argumentTypes, method, substitutor, place, eraseParameterTypes) !=
           Applicability.inapplicable;
  }

  public static Applicability isApplicableConcrete(PsiType @Nullable [] argumentTypes,
                                                   @NotNull PsiMethod method,
                                                   PsiSubstitutor substitutor,
                                                   PsiElement place,
                                                   final boolean eraseParameterTypes) {
    if (argumentTypes == null) return Applicability.canBeApplicable;
    GrSignature signature = GrClosureSignatureUtil.createSignature(method, substitutor, eraseParameterTypes, place);

    Applicability result = GroovyApplicabilityProvider.checkProviders(argumentTypes, method);
    if (result != null) return result;

    result = GrClosureSignatureUtil.isSignatureApplicableConcrete(singletonList(signature), argumentTypes, place);
    if (result != Applicability.inapplicable) {
      if (eraseParameterTypes || isTypeArgumentsApplicable(method.getTypeParameters(), substitutor, place)) {
        return result;
      }
    }

    return Applicability.inapplicable;
  }

  public static @Nullable GrArgumentList getArgumentsList(@Nullable PsiElement methodRef) {
    if (methodRef == null) return null;

    if (methodRef instanceof GrEnumConstant) return ((GrEnumConstant)methodRef).getArgumentList();
    PsiElement parent = methodRef.getParent();
    if (parent instanceof GrCall) {
      return ((GrCall)parent).getArgumentList();
    }
    if (parent instanceof GrAnonymousClassDefinition) {
      return ((GrAnonymousClassDefinition)parent).getArgumentListGroovy();
    }
    return null;
  }

  public static PsiType @Nullable [] getArgumentTypes(@Nullable PsiElement place, boolean nullAsBottom) {
    return getArgumentTypes(place, nullAsBottom, null);
  }

  public static PsiType @Nullable [] getArgumentTypes(@Nullable PsiElement place,
                                                      boolean nullAsBottom,
                                                      @Nullable GrExpression stopAt) {
    PsiElement parent = place instanceof GrEnumConstant ? place : place != null ? place.getParent() : null;

    if (parent instanceof GrIndexProperty index) {
      GrArgumentList list = index.getArgumentList();
      PsiType[] argTypes = getArgumentTypes(list.getNamedArguments(), list.getExpressionArguments(), GrClosableBlock.EMPTY_ARRAY, nullAsBottom, stopAt);
      if (isLValue(index) && argTypes != null) {
        PsiType rawInitializer = TypeInferenceHelper.getInitializerTypeFor(index);

        PsiType initializer = notNullizeType(rawInitializer, nullAsBottom, index);
        return ArrayUtil.append(argTypes, initializer);
      }
      else {
        return argTypes;
      }
    }
    if (parent instanceof GrCall call) {
      GrNamedArgument[] namedArgs = call.getNamedArguments();
      GrExpression[] expressions = call.getExpressionArguments();
      GrClosableBlock[] closures = call.getClosureArguments();

      return getArgumentTypes(namedArgs, expressions, closures, nullAsBottom, stopAt);
    }
    else if (parent instanceof GrAnonymousClassDefinition) {
      final GrArgumentList argList = ((GrAnonymousClassDefinition)parent).getArgumentListGroovy();
      if (argList == null) {
        return getArgumentTypes(GrNamedArgument.EMPTY_ARRAY, GrExpression.EMPTY_ARRAY, GrClosableBlock.EMPTY_ARRAY, nullAsBottom, stopAt
        );
      }
      else {
        return getArgumentTypes(argList.getNamedArguments(), argList.getExpressionArguments(), GrClosableBlock.EMPTY_ARRAY, nullAsBottom, stopAt
        );
      }
    }
    else if (parent instanceof GrBinaryExpression) {
      GrExpression right = ((GrBinaryExpression)parent).getRightOperand();
      PsiType type = right != null ? right.getType() : null;
      return new PsiType[]{notNullizeType(type, nullAsBottom, parent)};
    } else if (parent instanceof GrAssignmentExpression) {
      PsiType type = ((GrAssignmentExpression)parent).getType();
      return new PsiType[]{notNullizeType(type, nullAsBottom, parent)};
    }

    return null;
  }

  @Contract("_, false, _ -> !null; !null, _, _ -> !null; null, true, _ -> null")
  private static PsiType notNullizeType(PsiType type, boolean acceptNull, PsiElement context) {
    return type != null || acceptNull ? type : TypesUtil.getJavaLangObject(context);
  }

  public static PsiType @Nullable [] getArgumentTypes(GrArgumentList argList) {
    return getArgumentTypes(argList, false, null);
  }

  public static PsiType @Nullable [] getArgumentTypes(GrNamedArgument @NotNull [] namedArgs,
                                                      GrExpression @NotNull [] expressions,
                                                      GrClosableBlock @NotNull [] closures,
                                                      boolean nullAsBottom,
                                                      @Nullable GrExpression stopAt) {
    List<PsiType> result = new ArrayList<>();

    if (namedArgs.length > 0) {
      GrNamedArgument context = namedArgs[0];
      result.add(GrMapType.createFromNamedArgs(context, namedArgs));
    }

    for (GrExpression expression : expressions) {
      PsiType type = expression.getType();
      if (expression instanceof GrSpreadArgument) {
        if (type instanceof GrTupleType) {
          result.addAll(((GrTupleType)type).getComponentTypes());
        }
        else {
          return null;
        }
      }
      else {
        if (type == null) {
          result.add(nullAsBottom ? null : TypesUtil.getJavaLangObject(expression));
        }
        else {
          if (stopAt == expression) {
            type = TypeConversionUtil.erasure(type);
          }
          result.add(type);
        }
      }

      if (stopAt == expression) {
        return result.toArray(PsiType.createArray(result.size()));
      }
    }

    for (GrClosableBlock closure : closures) {
      PsiType closureType = closure.getType();
      if (closureType != null) {
        if (stopAt == closure) {
          closureType = TypeConversionUtil.erasure(closureType);
        }
        result.add(notNullizeType(closureType, nullAsBottom, closure));
      }
      if (stopAt == closure) {
        break;
      }
    }

    return result.toArray(PsiType.createArray(result.size()));
  }

  public static @Nullable PsiClass getJavaLangClass(PsiElement resolved, GlobalSearchScope scope) {
    return JavaPsiFacade.getInstance(resolved.getProject()).findClass(JAVA_LANG_CLASS, scope);
  }

  public static boolean isValidReferenceName(@NotNull String text) {
    final GroovyLexer lexer = new GroovyLexer();
    lexer.start(text);
    return TokenSets.REFERENCE_NAMES_WITHOUT_NUMBERS.contains(lexer.getTokenType()) && lexer.getTokenEnd() == text.length();
  }

  public static boolean isAccessible(@NotNull PsiElement place, @NotNull PsiMember member) {
    if (DynamicMemberUtils.isDynamicElement(member)) {
      return true;
    }

    return AccessibilityKt.isAccessible(member, place);
  }

  public static void reformatCode(final PsiElement element) {
    final TextRange textRange = element.getTextRange();

    PsiFile file = element.getContainingFile();
    FileViewProvider viewProvider = file.getViewProvider();

    if (viewProvider instanceof MultiplePsiFilesPerDocumentFileViewProvider multiProvider) {
      file = multiProvider.getPsi(multiProvider.getBaseLanguage());
      LOG.assertTrue(file != null, element + " " + multiProvider.getBaseLanguage());
    }

    try {
      CodeStyleManager.getInstance(element.getProject())
        .reformatText(file, textRange.getStartOffset(), textRange.getEndOffset());
    }
    catch (IncorrectOperationException e) {
      LOG.error(e);
    }
  }

  public static Iterable<PsiClass> iterateSupers(final @NotNull PsiClass psiClass, final boolean includeSelf) {
    return new Iterable<>() {
      @Override
      public Iterator<PsiClass> iterator() {
        //noinspection SSBasedInspection
        return new Iterator<>() {
          final IntArrayList indices = new IntArrayList();
          final Stack<PsiClassType[]> superTypesStack = new Stack<>();
          final Set<PsiClass> visited = new HashSet<>();
          PsiClass current;
          boolean nextObtained;

          {
            if (includeSelf) {
              current = psiClass;
              nextObtained = true;
            }
            else {
              current = null;
              nextObtained = false;
            }

            pushSuper(psiClass);
          }

          @Override
          public boolean hasNext() {
            nextElement();
            return current != null;
          }

          private void nextElement() {
            if (nextObtained) return;

            nextObtained = true;
            while (!superTypesStack.empty()) {
              assert !indices.isEmpty();

              int i = indices.popInt();
              PsiClassType[] superTypes = superTypesStack.peek();
              while (i < superTypes.length) {
                PsiClass clazz = superTypes[i].resolve();
                if (clazz != null && !visited.contains(clazz)) {
                  current = clazz;
                  visited.add(clazz);
                  indices.push(i + 1);
                  pushSuper(clazz);
                  return;
                }
                i++;
              }

              superTypesStack.pop();
            }

            current = null;
          }

          private void pushSuper(PsiClass clazz) {
            superTypesStack.push(clazz.getSuperTypes());
            indices.push(0);
          }

          @Override
          public @NotNull PsiClass next() {
            nextElement();
            nextObtained = false;
            if (current == null) throw new NoSuchElementException();
            return current;
          }

          @Override
          public void remove() {
            throw new IllegalStateException("should not be called");
          }
        };
      }
    };
  }

  public static @Nullable PsiClass getContextClass(@Nullable PsiElement element) {
    PsiElement context = element;
    while (context != null) {
      if (context instanceof GrAnonymousClassDefinition
          && PsiTreeUtil.isAncestor(((GrAnonymousClassDefinition)context).getArgumentListGroovy(), element, false)) {
        context = context.getContext();
        continue;
      }

      if (context instanceof PsiClass && !isInDummyFile(context)) {
        return (PsiClass)context;
      }
      else if (context instanceof GroovyFileBase && !isInDummyFile(context)) {
        return ((GroovyFileBase)context).getScriptClass();
      }

      context = context.getContext();
    }
    return null;
  }

  public static boolean isInDummyFile(@NotNull PsiElement context) {
    PsiFile file = context.getContainingFile();
    if (file == null) return false;

    String name = file.getName();
    return name.startsWith(GroovyPsiElementFactory.DUMMY_FILE_NAME);
  }

  public static @Nullable GroovyPsiElement getFileOrClassContext(PsiElement context) {
    while (context != null) {
      if (context instanceof GrTypeDefinition) {
        return (GroovyPsiElement)context;
      }
      else if (context instanceof GroovyFileBase && context.isPhysical()) {
        return (GroovyPsiElement)context;
      }

      context = context.getContext();
    }

    return null;
  }

  public static boolean mightBeLValue(@Nullable GrExpression expr) {
    if (expr instanceof GrParenthesizedExpression) return mightBeLValue(((GrParenthesizedExpression)expr).getOperand());

    if (expr instanceof GrTuple ||
        expr instanceof GrReferenceExpression ||
        expr instanceof GrIndexProperty ||
        expr instanceof GrPropertySelection) {
      return true;
    }

    if ((isThisOrSuperRef(expr)) &&
        GroovyConfigUtils.getInstance().isVersionAtLeast(expr, GroovyConfigUtils.GROOVY1_8)) {
      return true;
    }
    return false;
  }

  public static boolean isRawMethodCall(GrMethodCallExpression call) {
    final GroovyResolveResult result = call.advancedResolve();
    final PsiElement element = result.getElement();
    if (element == null) return false;
    if (element instanceof PsiMethod) {
      final GrExpression expression = call.getInvokedExpression();
      if (expression instanceof GrReferenceExpression) {
        final GroovyMethodCallReference reference = call.getImplicitCallReference();
        if (reference != null) {
          Argument receiver = reference.getReceiverArgument();
          PsiType receiverType = receiver == null ? null : receiver.getType();
          if (receiverType instanceof GroovyClosureType) {
            return isRawClosureCall(call, result, (GroovyClosureType)receiverType);
          }
        }
      }
      PsiType returnType = getSmartReturnType((PsiMethod)element);
      if (expression instanceof GrReferenceExpression && result.isInvokedOnProperty()) {
        if (returnType instanceof GroovyClosureType) {
          return isRawClosureCall(call, result, (GroovyClosureType)returnType);
        }
      }
      else {
        return isRawType(returnType, result.getSubstitutor());
      }
    }
    if (element instanceof PsiVariable) {
      GrExpression expression = call.getInvokedExpression();
      final PsiType type = expression.getType();
      if (type instanceof GroovyClosureType) {
        return isRawClosureCall(call, result, (GroovyClosureType)type);
      }
    }

    return false;
  }

  private static boolean isRawClosureCall(GrMethodCallExpression call, GroovyResolveResult result, GroovyClosureType closureType) {
    List<Argument> arguments = ArgumentsKt.getArguments(call);
    PsiType returnType = closureType.returnType(arguments);
    return isRawType(returnType, result.getSubstitutor());
  }

  public static boolean isRawFieldAccess(GrReferenceExpression ref) {
    PsiElement element = null;
    final GroovyResolveResult[] resolveResults = ref.multiResolve(false);
    if (resolveResults.length == 0) return false;
    final GroovyResolveResult resolveResult = resolveResults[0];
    if (resolveResult != null) {
      element = resolveResult.getElement();
    }
    if (element instanceof PsiField) {
      return isRawType(((PsiField)element).getType(), resolveResult.getSubstitutor());
    }
    else if (element instanceof GrAccessorMethod) {
      return isRawType(((GrAccessorMethod)element).getReturnType(), resolveResult.getSubstitutor());
    }
    return false;
  }

  private static boolean isRawIndexPropertyAccess(GrIndexProperty expr) {
    final GrExpression qualifier = expr.getInvokedExpression();
    final PsiType qualifierType = qualifier.getType();
    if (qualifierType instanceof PsiClassType classType) {

      if (InheritanceUtil.isInheritor(qualifierType, JAVA_UTIL_LIST)) {
        return com.intellij.psi.util.PsiUtil.extractIterableTypeParameter(qualifierType, false) == null;
      }

      if (InheritanceUtil.isInheritor(qualifierType, JAVA_UTIL_MAP)) {
        return com.intellij.psi.util.PsiUtil.substituteTypeParameter(qualifierType, JAVA_UTIL_MAP, 1, false) == null;
      }
      final PsiClassType.ClassResolveResult resolveResult = classType.resolveGenerics();
      GrExpression[] arguments = expr.getArgumentList().getExpressionArguments();
      PsiType[] argTypes = PsiType.createArray(arguments.length);
      for (int i = 0; i < arguments.length; i++) {
        PsiType argType = arguments[i].getType();
        if (argType == null) argType = TypesUtil.getJavaLangObject(expr);
        argTypes[i] = argType;
      }

      MethodResolverProcessor processor = new MethodResolverProcessor("getAt", expr, false, qualifierType, argTypes, PsiType.EMPTY_ARRAY);

      final PsiClass qClass = resolveResult.getElement();
      final ResolveState state = ResolveState.initial().put(PsiSubstitutor.KEY, PsiSubstitutor.EMPTY);
      if (qClass != null) {
        qClass.processDeclarations(processor, state, null, expr);
      }

      ResolveUtil.processNonCodeMembers(qualifierType, processor, qualifier, state);
      final GroovyResolveResult[] candidates = processor.getCandidates();
      PsiType type = null;
      if (candidates.length == 1) {
        final PsiElement element = candidates[0].getElement();
        if (element instanceof PsiMethod) {
          type = getSmartReturnType((PsiMethod)element);
        }
      }
      return isRawType(type, resolveResult.getSubstitutor());
    }
    return false;
  }

  public static boolean isRawClassMemberAccess(GrExpression expr) {
    expr = (GrExpression)skipParentheses(expr, false);

    if (expr instanceof GrMethodCallExpression) {
      return isRawMethodCall((GrMethodCallExpression)expr);
    }
    if (expr instanceof GrReferenceExpression) {
      return isRawFieldAccess((GrReferenceExpression)expr);
    }
    if (expr instanceof GrIndexProperty) {
      return isRawIndexPropertyAccess((GrIndexProperty)expr);
    }
    return false;
  }

  public static boolean isRawType(@Nullable PsiType type, @NotNull PsiSubstitutor substitutor) {
    if (type instanceof PsiClassType) {
      final PsiClass returnClass = ((PsiClassType)type).resolve();
      if (returnClass instanceof PsiTypeParameter typeParameter) {
        return substitutor.substitute(typeParameter) == null;
      }
    }
    return false;
  }

  public static boolean isWhiteSpaceOrLineFeed(PsiElement element) {
    if (element == null) return false;
    ASTNode node = element.getNode();
    if (node == null) return false;
    IElementType elementType = node.getElementType();
    return elementType == GroovyTokenTypes.mNLS || elementType == TokenType.WHITE_SPACE;
  }

  public static boolean isNewLine(PsiElement element) {
    return isWhiteSpaceOrLineFeed(element) && element.getText().contains("\n");
  }

  public static @Nullable PsiElement getPrevNonSpace(@NotNull PsiElement elem) {
    return PsiUtilKt.skipWhiteSpacesAndNewLinesBackward(elem);
  }

  public static @Nullable PsiElement getNextNonSpace(@NotNull PsiElement elem) {
    return PsiUtilKt.skipWhiteSpacesAndNewLinesForward(elem);
  }

  public static @NotNull PsiIdentifier getJavaNameIdentifier(@NotNull GrNamedElement namedElement) {
    final PsiElement element = namedElement.getNameIdentifierGroovy();
    JavaIdentifier identifier = element.getUserData(NAME_IDENTIFIER);
    if (identifier == null) {
      //noinspection SynchronizationOnLocalVariableOrMethodParameter
      synchronized (element) {
        identifier = element.getUserData(NAME_IDENTIFIER);
        if (identifier != null) {
          return identifier;
        }

        identifier = new JavaIdentifier(element.getManager(), element);
        element.putUserData(NAME_IDENTIFIER, identifier);
      }
    }
    return identifier;
  }

  public static @Nullable GrStatement findEnclosingStatement(@Nullable PsiElement context) {
    while (context != null) {
      if (isExpressionStatement(context)) return (GrStatement)context;
      context = PsiTreeUtil.getParentOfType(context, GrStatement.class, true);
    }
    return null;
  }

  public static boolean isMethodCall(GrMethodCall call, String methodName) {
    final GrExpression expression = call.getInvokedExpression();
    return expression instanceof GrReferenceExpression && methodName.equals(expression.getText().trim());
  }
  public static boolean hasEnclosingInstanceInScope(@NotNull PsiClass clazz, @Nullable PsiElement scope, boolean isSuperClassAccepted) {
    return findEnclosingInstanceClassInScope(clazz, scope, isSuperClassAccepted) != null;
  }

  public static PsiClass findEnclosingInstanceClassInScope(@NotNull PsiClass clazz, @Nullable PsiElement scope, boolean isInheritorOfClazzAccepted) {
    PsiElement place = scope;
    while (place != null && place != clazz && !(place instanceof PsiFile && place.isPhysical())) {
      if (place instanceof PsiClass) {
        if (isInheritorOfClazzAccepted) {
          if (InheritanceUtil.isInheritorOrSelf((PsiClass)place, clazz, true)) return (PsiClass)place;
        }
        else {
          if (clazz.getManager().areElementsEquivalent(place, clazz)) return (PsiClass)place;
        }
      }
      if (place instanceof PsiModifierListOwner && ((PsiModifierListOwner)place).hasModifierProperty(PsiModifier.STATIC)) return null;
      place = place.getContext();
    }
    if (clazz instanceof GroovyScriptClass && place == clazz.getContainingFile() || place == clazz) {
      return clazz;
    }
    return null;
  }


  public static @Nullable PsiElement skipWhitespacesAndComments(@Nullable PsiElement elem, boolean forward, boolean skipNLs) {
    return skipSet(elem, forward, TokenSets.WHITE_SPACES_OR_COMMENTS, skipNLs);
  }


  public static @Nullable PsiElement skipWhitespacesAndComments(@Nullable PsiElement elem, boolean forward) {
    return skipSet(elem, forward, TokenSets.WHITE_SPACES_OR_COMMENTS, true);
  }

  public static @Nullable PsiElement skipSet(PsiElement elem, boolean forward, TokenSet set, boolean skipNLs) {
    while (elem != null &&
           elem.getNode() != null &&
           set.contains(elem.getNode().getElementType()) &&
           (skipNLs || elem.getText().indexOf('\n') == -1)) {
      if (forward) {
        elem = elem.getNextSibling();
      }
      else {
        elem = elem.getPrevSibling();
      }
    }
    return elem;
  }

  public static @Nullable PsiElement skipSet(@NotNull PsiElement element, boolean forward, @NotNull TokenSet set) {
    do {
      if (forward) {
        element = element.getNextSibling();
      }
      else {
        element = element.getPrevSibling();
      }
    }
    while (element != null && element.getNode() != null && set.contains(element.getNode().getElementType()));

    return element;
  }

  public static @Nullable PsiElement skipWhitespaces(@Nullable PsiElement elem, boolean forward) {
    return skipSet(elem, forward, TokenSets.WHITE_SPACES_SET, true);
  }


  public static @Nullable PsiType getSmartReturnType(@NotNull PsiMethod method) {
    if (GdkMethodUtil.isMacro(method)) {
      return null;
    }
    if (method instanceof GrMethod) {
      return ((GrMethod)method).getInferredReturnType();
    }
    else if (method instanceof GrAccessorMethod) {
      return ((GrAccessorMethod)method).getInferredReturnType();
    }
    else if (method instanceof GrGdkMethod) {
      return getSmartReturnType(((GrGdkMethod)method).getStaticMethod());
    }
    else {
      return method.getReturnType();
    }
  }

  public static boolean isClosurePropertyGetter(PsiMethod method) {
    String methodName = method.getName();
    if (methodName.startsWith("get") && GroovyPropertyUtils.isGetterName(methodName)) { // exclude isXXX()
      PsiModifierList modifiers = method.getModifierList();

      if (!modifiers.hasModifierProperty(PsiModifier.STATIC)
          && !modifiers.hasModifierProperty(PsiModifier.PRIVATE)
          && !modifiers.hasModifierProperty(PsiModifier.PROTECTED)
          && method.getParameterList().isEmpty()) {
        final PsiType type = getSmartReturnType(method);
        if (type != null && (TypesUtil.isClassType(type, JAVA_LANG_OBJECT, GROOVY_LANG_CLOSURE))) {
          return true;
        }
      }
    }

    return false;
  }

  public static boolean isMethodUsage(PsiElement element) {
    if (element instanceof GrEnumConstant) return true;
    if (!(element instanceof GrReferenceElement)) return false;
    PsiElement parent = element.getParent();
    if (parent instanceof GrCall) {
      return true;
    }
    else if (parent instanceof GrAnonymousClassDefinition) {
      return element.equals(((GrAnonymousClassDefinition)parent).getBaseClassReferenceGroovy());
    }
    return false;
  }

  public static boolean isAccessedForReading(GrExpression expr) {
    return !isLValue(expr);
  }

  public static boolean isAccessedForWriting(GrExpression expr) {
    return isLValue(expr) || isUsedInIncOrDec(expr);
  }

  public static boolean isUsedInIncOrDec(GrExpression expr) {
    PsiElement parent = PsiTreeUtil.skipParentsOfType(expr, GrParenthesizedExpression.class);
    return isPlusPlusOrMinusMinus(parent);
  }

  @Contract("null -> false")
  public static boolean isPlusPlusOrMinusMinus(@Nullable PsiElement parent) {
    if (parent instanceof GrUnaryExpression) {
      IElementType tokenType = ((GrUnaryExpression)parent).getOperationTokenType();
