// 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.codeInsight.daemon.impl.quickfix;

import com.intellij.codeInsight.*;
import com.intellij.codeInsight.completion.proc.VariablesProcessor;
import com.intellij.codeInsight.daemon.QuickFixBundle;
import com.intellij.codeInsight.generation.OverrideImplementUtil;
import com.intellij.codeInsight.generation.PsiGenerationInfo;
import com.intellij.codeInsight.intention.impl.CreateClassDialog;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.codeInsight.lookup.LookupFocusDegree;
import com.intellij.codeInsight.template.*;
import com.intellij.codeInsight.template.ExpressionUtil;
import com.intellij.codeInspection.CommonQuickFixBundle;
import com.intellij.core.JavaPsiBundle;
import com.intellij.ide.fileTemplates.FileTemplate;
import com.intellij.ide.fileTemplates.FileTemplateManager;
import com.intellij.ide.fileTemplates.FileTemplateUtil;
import com.intellij.ide.fileTemplates.JavaTemplateUtil;
import com.intellij.ide.scratch.ScratchUtil;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorModificationUtilEx;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.java.JavaFeature;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.codeStyle.SuggestedNameInfo;
import com.intellij.psi.codeStyle.VariableKind;
import com.intellij.psi.impl.source.PostprocessReformattingAspect;
import com.intellij.psi.scope.util.PsiScopesUtil;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.PsiShortNamesCache;
import com.intellij.psi.search.searches.ClassInheritorsSearch;
import com.intellij.psi.statistics.JavaStatisticsManager;
import com.intellij.psi.statistics.StatisticsManager;
import com.intellij.psi.util.ProximityLocation;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiTypesUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.proximity.PsiProximityComparator;
import com.intellij.util.ArrayUtil;
import com.intellij.util.CommonJavaRefactoringUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;

public final class CreateFromUsageUtils {
  private static final Logger LOG = Logger.getInstance(CreateFromUsageUtils.class);
  private static final int MAX_GUESSED_MEMBERS_COUNT = 10;
  private static final int MAX_RAW_GUESSED_MEMBERS_COUNT = 2 * MAX_GUESSED_MEMBERS_COUNT;

  static boolean isValidReference(PsiReference reference, boolean unresolvedOnly) {
    if (!(reference instanceof PsiJavaReference)) return false;
    JavaResolveResult[] results = ((PsiJavaReference)reference).multiResolve(false);
    if(results.length == 0) return false;
    if (!unresolvedOnly) {
      for (JavaResolveResult result : results) {
        if (!result.isValidResult()) return false;
        if (result.getElement() instanceof PsiPackage) return false;
      }
    }
    return true;
  }

  public static boolean isValidMethodReference(PsiReference reference, PsiMethodCallExpression call) {
    if (!(reference instanceof PsiJavaReference)) return false;
    try {
      JavaResolveResult candidate = ((PsiJavaReference) reference).advancedResolve(false);
      PsiElement result = candidate.getElement();
      return result instanceof PsiMethod && PsiUtil.isApplicable((PsiMethod)result, candidate.getSubstitutor(), call.getArgumentList());
    }
    catch (ClassCastException cce) {
      // rear case
      return false;
    }
  }

  public static void setupMethodBody(@NotNull PsiMethod method) throws IncorrectOperationException {
    PsiClass aClass = method.getContainingClass();
    FileTemplate template = FileTemplateManager.getInstance(method.getProject()).getCodeTemplate(JavaTemplateUtil.TEMPLATE_FROM_USAGE_METHOD_BODY);
    setupMethodBody(method, aClass, template, null);
  }

  public static void setupMethodBody(final PsiMethod method, @NotNull ModPsiUpdater updater) throws IncorrectOperationException {
    PsiClass aClass = method.getContainingClass();
    FileTemplate template = FileTemplateManager.getInstance(method.getProject()).getCodeTemplate(JavaTemplateUtil.TEMPLATE_FROM_USAGE_METHOD_BODY);
    setupMethodBody(method, aClass, template, updater);
  }

  public static void setupMethodBody(final PsiMethod method, final PsiClass aClass, final FileTemplate template) 
    throws IncorrectOperationException {
    setupMethodBody(method, aClass, template, null);
  }

  private static void setupMethodBody(final PsiMethod method, final PsiClass aClass, 
                                      final FileTemplate template, @Nullable ModPsiUpdater updater) 
    throws IncorrectOperationException {
    PsiType returnType = method.getReturnType();
    if (returnType == null) {
      returnType = PsiTypes.voidType();
    }

    JVMElementFactory factory = JVMElementFactories.getFactory(aClass.getLanguage(), aClass.getProject());

    LOG.assertTrue(!aClass.isInterface() ||
                   PsiUtil.isAvailable(JavaFeature.EXTENSION_METHODS, method) ||
                   method.getLanguage() != JavaLanguage.INSTANCE, "Interface bodies should be already set up");

    FileType fileType = FileTypeManager.getInstance().getFileTypeByExtension(template.getExtension());
    Map<String, Object> properties = FileTemplateManager.getInstance(method.getProject()).getDefaultContextMap();
    properties.put(FileTemplate.ATTRIBUTE_RETURN_TYPE, returnType.getPresentableText());
    properties.put(FileTemplate.ATTRIBUTE_DEFAULT_RETURN_VALUE, PsiTypesUtil.getDefaultValueOfType(returnType));

    JavaTemplateUtil.setClassAndMethodNameProperties(properties, aClass, method);

    @NonNls String methodText;
    CodeStyleManager csManager = CodeStyleManager.getInstance(method.getProject());
    try {
      String bodyText = template.getText(properties);
      if (!bodyText.isEmpty()) bodyText += "\n";
      methodText = returnType.getPresentableText() + " foo () {\n" + bodyText + "}";
      methodText = FileTemplateUtil.indent(methodText, method.getProject(), fileType);
    }
    catch (ProcessCanceledException e) {
      throw e;
    }
    catch (Exception e) {
      throw new IncorrectOperationException("Failed to parse file template", (Throwable)e);
    }

    PsiMethod m;
    try {
      m = factory.createMethodFromText(methodText, aClass);
    }
    catch (IncorrectOperationException e) {
      if (updater == null) {
        ApplicationManager.getApplication().invokeLater(
          () -> Messages.showErrorDialog(QuickFixBundle.message("new.method.body.template.error.text"),
                                         QuickFixBundle.message("new.method.body.template.error.title")));
      } else {
        updater.cancel(QuickFixBundle.message("new.method.body.template.error.text"));
      }
      return;
    }

    PsiElement newBody = m.getBody();
    LOG.assertTrue(newBody != null);

    PsiElement oldBody = method.getBody();
    if (oldBody == null) {
      PsiElement last = method.getLastChild();
      if (last instanceof PsiErrorElement &&
          JavaPsiBundle.message("expected.lbrace.or.semicolon").equals(((PsiErrorElement)last).getErrorDescription())) {
        oldBody = last;
      }
    }
    if (oldBody != null) {
      oldBody.replace(newBody);
    }
    else {
      method.add(newBody);
    }

    csManager.reformat(method);
  }

  public static void setupEditor(@NotNull PsiMethod method, @NotNull Editor newEditor) {
    PsiCodeBlock body = method.getBody();
    if (body != null) {
      setupEditor(body, newEditor);
    }
  }

  public static void setupEditor(@NotNull PsiCodeBlock body, @NotNull Editor newEditor) {
    PsiElement l = PsiTreeUtil.skipWhitespacesForward(body.getLBrace());
    PsiElement r = PsiTreeUtil.skipWhitespacesBackward(body.getRBrace());
    if (l != null && r != null) {
      int start = l.getTextRange().getStartOffset();
      int end = r.getTextRange().getEndOffset();
      newEditor.getCaretModel().moveToOffset(Math.max(start, end));
      if (end < start) {
        newEditor.getCaretModel().moveToOffset(end + 1);
        CodeStyleManager styleManager = CodeStyleManager.getInstance(body.getProject());
        PsiFile containingFile = body.getContainingFile();
        final String lineIndent = Objects.requireNonNullElse(styleManager.getLineIndent(containingFile, end), "");
        PsiDocumentManager manager = PsiDocumentManager.getInstance(body.getProject());
        manager.doPostponedOperationsAndUnblockDocument(newEditor.getDocument());
        EditorModificationUtilEx.insertStringAtCaret(newEditor, lineIndent);
        EditorModificationUtilEx.insertStringAtCaret(newEditor, "\n", false, false);
      }
      else {
        //correct position caret for groovy and java methods
        if (body.getParent() instanceof PsiMethod) {
          final PsiGenerationInfo<PsiMethod> info = OverrideImplementUtil.createGenerationInfo((PsiMethod)body.getParent());
          info.positionCaret(newEditor, true);
        }
      }
      newEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
    }
  }

  public static void setupEditor(@NotNull PsiCodeBlock body, @NotNull ModPsiUpdater updater) {
    PsiElement l = PsiTreeUtil.skipWhitespacesForward(body.getLBrace());
    PsiElement r = PsiTreeUtil.skipWhitespacesBackward(body.getRBrace());
    if (l != null && r != null) {
      int start = l.getTextRange().getStartOffset();
      int end = r.getTextRange().getEndOffset();
      updater.moveCaretTo(Math.max(start, end));
      if (end < start) {
        updater.moveCaretTo(end + 1);
        CodeStyleManager styleManager = CodeStyleManager.getInstance(body.getProject());
        PsiFile containingFile = body.getContainingFile();
        final String lineIndent = Objects.requireNonNullElse(styleManager.getLineIndent(containingFile, end), "");
        PsiDocumentManager manager = PsiDocumentManager.getInstance(body.getProject());
        Document document = body.getContainingFile().getViewProvider().getDocument();
        manager.doPostponedOperationsAndUnblockDocument(document);
        document.insertString(updater.getCaretOffset(), lineIndent + "\n");
        updater.moveCaretTo(updater.getCaretOffset() + lineIndent.length());
      }
      else {
        //correct position caret for groovy and java methods
        if (body.getParent() instanceof PsiMethod) {
          final PsiGenerationInfo<PsiMethod> info = OverrideImplementUtil.createGenerationInfo((PsiMethod)body.getParent());
          info.positionCaret(updater, true);
        }
      }
    }
  }

  static void setupMethodParameters(PsiMethod method, TemplateBuilder builder, PsiExpressionList argumentList,
                                    PsiSubstitutor substitutor) throws IncorrectOperationException {
    if (argumentList == null) return;
    PsiExpression[] args = argumentList.getExpressions();

    setupMethodParameters(method, builder, argumentList, substitutor, args);
  }

  public static void setupMethodParameters(final PsiMethod method, final TemplateBuilder builder, final PsiElement contextElement,
                                           final PsiSubstitutor substitutor, final PsiExpression[] arguments) {
    setupMethodParameters(method, builder, contextElement, substitutor, ContainerUtil.map(arguments, arg -> new Pair<>(arg, null)));
  }

  static void setupMethodParameters(final PsiMethod method, final TemplateBuilder builder, final PsiElement contextElement,
                                    final PsiSubstitutor substitutor, final List<? extends Pair<PsiExpression, PsiType>> arguments)
    throws IncorrectOperationException {

    final PsiManager psiManager = method.getManager();
    final Project project = psiManager.getProject();

    JVMElementFactory factory = JVMElementFactories.getFactory(method.getLanguage(), project);
    if (factory == null) return;

    PsiParameterList parameterList = method.getParameterList();

    GlobalSearchScope resolveScope = method.getResolveScope();

    GuessTypeParameters guesser = new GuessTypeParameters(project, JavaPsiFacade.getElementFactory(project), builder, substitutor);

    PostprocessReformattingAspect postprocessReformattingAspect = PostprocessReformattingAspect.getInstance(project);
    final PsiClass containingClass = method.getContainingClass();
    final boolean isInterface = containingClass != null && containingClass.isInterface();
    //255 is the maximum number of method parameters
    for (int i = 0; i < Math.min(arguments.size(), 255); i++) {
      Pair<PsiExpression, PsiType> arg = arguments.get(i);
      PsiExpression exp = arg.first;

      PsiType argType = exp == null ? arg.second : CommonJavaRefactoringUtil.getTypeByExpression(exp);
      SuggestedNameInfo suggestedInfo = JavaCodeStyleManager.getInstance(project).suggestVariableName(
        VariableKind.PARAMETER, null, exp, argType);
      @NonNls String[] names = suggestedInfo.names; //TODO: callback about used name

      if (names.length == 0) {
        names = new String[]{"p" + i};
      }

      argType = getParameterTypeByArgumentType(argType, psiManager, resolveScope);
      PsiParameter parameter = parameterList.getParameter(i);
      if (parameter == null) {
        // Remove top-level annotations. They will be added by setupTypeElement again
        PsiParameter param = factory.createParameter(names[0], argType.annotate(TypeAnnotationProvider.EMPTY));
        if (isInterface) {
          PsiUtil.setModifierProperty(param, PsiModifier.FINAL, false);
        }
        parameter = postprocessReformattingAspect.postponeFormattingInside(() -> (PsiParameter) parameterList.add(param));
      }

      ExpectedTypeInfo info = ExpectedTypesProvider.createInfo(argType, ExpectedTypeInfo.TYPE_OR_SUPERTYPE, argType, TailTypes.noneType());

      PsiElement context = PsiTreeUtil.getParentOfType(contextElement, PsiClass.class, PsiMethod.class);
      guesser.setupTypeElement(parameter.getTypeElement(), new ExpectedTypeInfo[]{info}, context, containingClass);

      Expression expression = new ParameterNameExpression(names);
      builder.replaceElement(parameter.getNameIdentifier(), expression);
    }
  }

  /**
   * Get a type suitable for parameter declaration based on given argument type
   *
   * @param argType argument type
   * @param psiManager psiManager to use
   * @param resolveScope type resolve scope
   * @return a type suitable for parameter declaration; java.lang.Object if supplied argument type is null
   */
  public static @NotNull PsiType getParameterTypeByArgumentType(@Nullable PsiType argType,
                                                       @NotNull PsiManager psiManager,
                                                       @NotNull GlobalSearchScope resolveScope) {
    if (argType instanceof PsiDisjunctionType) {
      argType = ((PsiDisjunctionType)argType).getLeastUpperBound();
    }
    else if (argType instanceof PsiWildcardType) {
      argType = ((PsiWildcardType)argType).getBound();
    }
    if (argType == null || PsiTypes.nullType().equals(argType) || LambdaUtil.notInferredType(argType)) {
      argType = PsiType.getJavaLangObject(psiManager, resolveScope);
    }
    return argType;
  }

  public static @Nullable PsiClass createClass(final PsiJavaCodeReferenceElement referenceElement,
                                               final CreateClassKind classKind,
                                               final String superClassName) {
    assert !ApplicationManager.getApplication().isWriteAccessAllowed() : "You must not run createClass() from under write action";
    final String name = referenceElement.getReferenceName();

    String qualifierName;
    final PsiElement qualifierElement;
    PsiElement qualifier = referenceElement.getQualifier();
    if (qualifier instanceof PsiJavaCodeReferenceElement) {
      qualifierName = ((PsiJavaCodeReferenceElement)qualifier).getQualifiedName();
      qualifierElement = ((PsiJavaCodeReferenceElement)qualifier).resolve();
      if (qualifierElement instanceof PsiClass) {
        if (!FileModificationService.getInstance().preparePsiElementForWrite(qualifierElement)) return null;

        return WriteAction.compute(() -> createClassInQualifier((PsiClass)qualifierElement, classKind, name, referenceElement, superClassName));
      }
    }
    else {
      qualifierName = null;
      qualifierElement = null;
    }

    final PsiManager manager = referenceElement.getManager();
    final PsiFile sourceFile = referenceElement.getContainingFile();
    final Module module = ModuleUtilCore.findModuleForPsiElement(sourceFile);
    if (qualifierName == null) {
      PsiPackage aPackage = findTargetPackage(qualifierElement, manager, sourceFile);
      if (aPackage == null) return null;
      qualifierName = aPackage.getQualifiedName();
    }
    final PsiDirectory targetDirectory;
    if (!ApplicationManager.getApplication().isUnitTestMode() &&
        !ScratchUtil.isScratch(referenceElement.getContainingFile().getVirtualFile())) {
      Project project = manager.getProject();
      String title = CommonQuickFixBundle.message("fix.create.title", StringUtil.capitalize(classKind.getDescriptionAccusative()));

      CreateClassDialog dialog = new CreateClassDialog(project, title, name, qualifierName, classKind, false, module){
        @Override
        protected @Nullable PsiDirectory getBaseDir(String packageName) {
          return sourceFile.getContainingDirectory();
        }

        @Override
        protected boolean reportBaseInSourceSelectionInTest() {
          return true;
        }
      };
      dialog.show();
      if (dialog.getExitCode() != DialogWrapper.OK_EXIT_CODE) return null;

      targetDirectory = dialog.getTargetDirectory();
      if (targetDirectory == null) return null;
    }
    else {
      if (!FileModificationService.getInstance().prepareFileForWrite(sourceFile)) {
        return null;
      }
      targetDirectory = null;
    }
    return createClass(classKind, targetDirectory, name, manager, referenceElement, sourceFile, superClassName);
  }

  private static @Nullable PsiPackage findTargetPackage(PsiElement qualifierElement, PsiManager manager, PsiFile sourceFile) {
    PsiPackage aPackage = null;
    if (qualifierElement instanceof PsiPackage) {
      aPackage = (PsiPackage)qualifierElement;
    }
    else {
      final PsiDirectory directory = sourceFile.getContainingDirectory();
      if (directory != null) {
        aPackage = JavaDirectoryService.getInstance().getPackage(directory);
      }

      if (aPackage == null) {
        aPackage = JavaPsiFacade.getInstance(manager.getProject()).findPackage("");
      }
    }
    if (aPackage == null) return null;
    return aPackage;
  }

  private static PsiClass createClassInQualifier(PsiClass psiClass,
                                                 CreateClassKind classKind,
                                                 String name,
                                                 PsiJavaCodeReferenceElement referenceElement, 
                                                 @Nullable String superClassName) {
    PsiManager manager = psiClass.getManager();
    PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(manager.getProject());
    PsiClass result = classKind == CreateClassKind.INTERFACE ? elementFactory.createInterface(name) :
                      classKind == CreateClassKind.CLASS ? elementFactory.createClass(name) :
                      classKind == CreateClassKind.ANNOTATION ? elementFactory.createAnnotationType(name) :
                      classKind == CreateClassKind.RECORD ? elementFactory.createRecord(name) :
                      elementFactory.createEnum(name);
    CreateFromUsageBaseFix.setupGenericParameters(result, referenceElement);
    result = (PsiClass)CodeStyleManager.getInstance(manager.getProject()).reformat(result);
    if (!StringUtil.isEmpty(superClassName)) {
      setupSuperClassReference(result, superClassName);
    }
    return (PsiClass) psiClass.add(result);
  }

  public static PsiClass createClass(final CreateClassKind classKind,
                                     final PsiDirectory directory,
                                     final String name,
                                     final PsiManager manager,
                                     final @NotNull PsiElement contextElement,
                                     final PsiFile sourceFile,
                                     final String superClassName) {
    final JavaPsiFacade facade = JavaPsiFacade.getInstance(manager.getProject());
    final PsiElementFactory factory = facade.getElementFactory();

    return WriteAction.compute(() -> {
        try {
          PsiClass targetClass;
          if (directory != null) {
            try {
              targetClass = classKind.createInDirectory(directory, name);
            }
            catch (final IncorrectOperationException e) {
              scheduleFileOrPackageCreationFailedMessageBox(e, name, directory, false);
              return null;
            }
            if (!facade.getResolveHelper().isAccessible(targetClass, contextElement, null)) {
              PsiUtil.setModifierProperty(targetClass, PsiModifier.PUBLIC, true);
            }
          }
          else { //tests
            PsiClass aClass = classKind.create(factory, name);
            targetClass = (PsiClass)sourceFile.add(aClass);
            if (ScratchUtil.isScratch(sourceFile.getVirtualFile())) {
              PsiUtil.setModifierProperty(targetClass, PsiModifier.PACKAGE_LOCAL, true);
            }
          }

          if (StringUtil.isNotEmpty(superClassName)  &&
              (classKind != CreateClassKind.ENUM || !superClassName.equals(CommonClassNames.JAVA_LANG_ENUM)) &&
              (classKind != CreateClassKind.RECORD || !superClassName.equals(CommonClassNames.JAVA_LANG_RECORD))) {
            setupSuperClassReference(targetClass, superClassName);
          }
          if (contextElement instanceof PsiJavaCodeReferenceElement) {
            CreateFromUsageBaseFix.setupGenericParameters(targetClass, (PsiJavaCodeReferenceElement)contextElement);
          }
          return targetClass;
        }
        catch (IncorrectOperationException e) {
          LOG.error(e);
          return null;
        }
      });
  }

  public static void setupSuperClassReference(PsiClass targetClass, String superClassName) {
    JavaPsiFacade facade = JavaPsiFacade.getInstance(targetClass.getProject());
    PsiElementFactory factory = facade.getElementFactory();
    final PsiClass superClass =
      facade.findClass(superClassName, targetClass.getResolveScope());
    final PsiJavaCodeReferenceElement superClassReference = factory.createReferenceElementByFQClassName(superClassName, targetClass.getResolveScope());
    final PsiReferenceList list = targetClass.isInterface() || superClass == null || !superClass.isInterface() ?
                                  targetClass.getExtendsList() : targetClass.getImplementsList();
    list.add(superClassReference);
  }

  public static void scheduleFileOrPackageCreationFailedMessageBox(final IncorrectOperationException e, final String name, final PsiDirectory directory,
                                                      final boolean isPackage) {
    ApplicationManager.getApplication().invokeLater(() -> Messages.showErrorDialog(QuickFixBundle.message(
      isPackage ? "cannot.create.java.package.error.text" : "cannot.create.java.file.error.text", name, directory.getVirtualFile().getName(), e.getLocalizedMessage()),
                                                                               QuickFixBundle.message(
                               isPackage ? "cannot.create.java.package.error.title" : "cannot.create.java.file.error.title")));
  }

  @SafeVarargs
  public static PsiReferenceExpression @NotNull [] collectExpressions(final PsiExpression expression, Class<? extends PsiElement> @NotNull ... scopes) {
    PsiElement parent = PsiTreeUtil.getParentOfType(expression, scopes);

    final List<PsiReferenceExpression> result = new ArrayList<>();
    JavaRecursiveElementWalkingVisitor visitor = new JavaRecursiveElementWalkingVisitor() {
      @Override public void visitReferenceExpression(@NotNull PsiReferenceExpression expr) {
        if (expression instanceof PsiReferenceExpression &&
            (expr.getParent() instanceof PsiMethodCallExpression == expression.getParent() instanceof PsiMethodCallExpression)) {
          if (Objects.equals(expr.getReferenceName(), ((PsiReferenceExpression)expression).getReferenceName()) && !isValidReference(expr, false)) {
            result.add(expr);
          }
        }
        visitElement(expr);
      }

      @Override public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expr) {
        if (expression instanceof PsiMethodCallExpression) {
          PsiReferenceExpression methodExpression = expr.getMethodExpression();
          if (Objects.equals(methodExpression.getReferenceName(),
                             ((PsiMethodCallExpression)expression).getMethodExpression().getReferenceName()) &&
              methodExpression.resolve() == ((PsiMethodCallExpression)expression).resolveMethod()) {
            result.add(expr.getMethodExpression());
          }
        }
        visitElement(expr);
      }
    };

    if (parent != null) {
      parent.accept(visitor);
    }
    return result.toArray(new PsiReferenceExpression[0]);
  }

  static PsiVariable @NotNull [] guessMatchingVariables(final PsiExpression expression) {
    List<ExpectedTypeInfo[]> typesList = new ArrayList<>();
    List<String> expectedMethodNames = new ArrayList<>();
    List<String> expectedFieldNames  = new ArrayList<>();

    getExpectedInformation(expression, typesList, expectedMethodNames, expectedFieldNames);

    final List<PsiVariable> list = new ArrayList<>();
    VariablesProcessor varproc = new VariablesProcessor("", true, list){
      @Override
      public boolean execute(@NotNull PsiElement element, @NotNull ResolveState state) {
        if(!(element instanceof PsiField) ||
           JavaPsiFacade.getInstance(element.getProject()).getResolveHelper().isAccessible((PsiField)element, expression, null)) {
          return super.execute(element, state);
        }
        return true;
      }
    };
    PsiScopesUtil.treeWalkUp(varproc, expression, null);
    PsiVariable[] allVars = varproc.getResultsAsArray();

    ExpectedTypeInfo[] infos = ExpectedTypeUtil.intersect(typesList);

    List<PsiVariable> result = new ArrayList<>();
    nextVar:
    for (PsiVariable variable : allVars) {
      PsiType varType = variable.getType();
      boolean matched = infos.length == 0;
      for (ExpectedTypeInfo info : infos) {
        if (ExpectedTypeUtil.matches(varType, info)) {
          matched = true;
          break;
        }
      }

      if (matched) {
        if (!expectedFieldNames.isEmpty() && !expectedMethodNames.isEmpty()) {
          if (!(varType instanceof PsiClassType)) continue;
          PsiClass aClass = ((PsiClassType)varType).resolve();
          if (aClass == null) continue;
          for (String name : expectedFieldNames) {
            if (aClass.findFieldByName(name, true) == null) continue nextVar;
          }

          for (String name : expectedMethodNames) {
            PsiMethod[] methods = aClass.findMethodsByName(name, true);
            if (methods.length == 0) continue nextVar;
          }
        }

        result.add(variable);
      }
    }

    return result.toArray(new PsiVariable[0]);
  }

  private static void getExpectedInformation(final PsiExpression expression,
                                             List<ExpectedTypeInfo[]> types,
                                             List<? super String> expectedMethodNames,
                                             List<? super String> expectedFieldNames) {
    Comparator<ExpectedTypeInfo> expectedTypesComparator = (o1, o2) -> compareExpectedTypes(o1, o2, expression);
    for (PsiExpression expr : collectExpressions(expression, PsiMember.class, PsiFile.class)) {
      PsiElement parent = PsiUtil.skipParenthesizedExprUp(expr.getParent());

      if (!(parent instanceof PsiReferenceExpression)) {
        boolean isAssignmentToFunctionalExpression = PsiUtil.isOnAssignmentLeftHand(expr) &&
                                                     ((PsiAssignmentExpression)PsiUtil.skipParenthesizedExprUp(parent)).getRExpression() instanceof PsiFunctionalExpression;
        PsiElement gParent = parent.getParent();
        PsiExpressionList expressionList = ObjectUtils
          .tryCast(PsiUtil.skipParenthesizedExprUp(isAssignmentToFunctionalExpression ? gParent : parent), PsiExpressionList.class);
        boolean forCompletion;
        if (expressionList != null) {
          forCompletion = true;
        }
        else if (parent instanceof PsiPolyadicExpression) {
          forCompletion = !(gParent instanceof PsiPolyadicExpression);
        }
        else {
          forCompletion = gParent instanceof PsiPolyadicExpression && !(gParent.getParent() instanceof PsiPolyadicExpression);
        }
        ExpectedTypeInfo[] someExpectedTypes = ExpectedTypesProvider.getExpectedTypes(expr, forCompletion);
        if (someExpectedTypes.length > 0) {
          Comparator<ExpectedTypeInfo> comparator = expectedTypesComparator;
          if (expressionList != null) {
            int argCount = expressionList.getExpressionCount();
            Comparator<ExpectedTypeInfo> mostSuitableMethodComparator =
              Comparator.comparingInt(typeInfo -> typeInfo.getCalledMethod().getParameterList().getParametersCount() == argCount ? 0 : 1);
            comparator = mostSuitableMethodComparator.thenComparing(comparator);
          }
          Arrays.sort(someExpectedTypes, comparator);
          types.add(someExpectedTypes);
        }
        continue;
      }

      String refName = ((PsiReferenceExpression)parent).getReferenceName();
      if (refName == null) {
        continue;
      }

      PsiElement pparent = PsiUtil.skipParenthesizedExprUp(parent.getParent());
      if (pparent instanceof PsiMethodCallExpression) {
        expectedMethodNames.add(refName);
        if (refName.equals("equals")) {
          ExpectedTypeInfo[] someExpectedTypes = equalsExpectedTypes((PsiMethodCallExpression)pparent);
          if (someExpectedTypes.length > 0) {
            Arrays.sort(someExpectedTypes, expectedTypesComparator);
            types.add(someExpectedTypes);
          }
        }
        continue;
      }

      if (pparent instanceof PsiVariable ||
          pparent instanceof PsiExpression) {
        expectedFieldNames.add(refName);
      }
    }
  }

  private static int compareExpectedTypes(ExpectedTypeInfo o1, ExpectedTypeInfo o2, PsiExpression expression) {
    PsiClass c1 = PsiUtil.resolveClassInType(o1.getDefaultType());
    PsiClass c2 = PsiUtil.resolveClassInType(o2.getDefaultType());
    if (c1 == null && c2 == null) return 0;
    if (c1 == null || c2 == null) return c1 == null ? -1 : 1;
    return compareMembers(c1, c2, expression);
  }

  private static ExpectedTypeInfo @NotNull [] equalsExpectedTypes(PsiMethodCallExpression methodCall) {
