// 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.impl.source.codeStyle;

import com.intellij.application.options.CodeStyle;
import com.intellij.codeInsight.JavaProjectCodeInsightSettings;
import com.intellij.codeInspection.dataFlow.ContractReturnValue;
import com.intellij.codeInspection.dataFlow.JavaMethodContractUtil;
import com.intellij.codeInspection.dataFlow.MethodContract;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.LiteralNameSuggester;
import com.intellij.openapi.util.text.PastParticiple;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.java.JavaRelease;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.*;
import com.intellij.psi.impl.CheckUtil;
import com.intellij.psi.impl.search.MethodDeepestSuperSearcher;
import com.intellij.psi.impl.source.SourceTreeToPsiMap;
import com.intellij.psi.impl.source.jsp.jspJava.JspxImportStatement;
import com.intellij.psi.statistics.JavaStatisticsManager;
import com.intellij.psi.util.*;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.BitUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.NameUtilCore;
import com.intellij.util.text.UniqueNameGenerator;
import com.siyeh.ig.psiutils.ExpressionUtils;
import com.siyeh.ig.psiutils.MethodCallUtils;
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.beans.Introspector;
import java.util.*;
import java.util.function.Predicate;

public class JavaCodeStyleManagerImpl extends JavaCodeStyleManager {
  private static final Logger LOG = Logger.getInstance(JavaCodeStyleManagerImpl.class);

  private static final @NonNls String IMPL_SUFFIX = "Impl";
  private static final @NonNls String GET_PREFIX = "get";
  private static final @NonNls String IS_PREFIX = "is";
  private static final @NonNls String FIND_PREFIX = "find";
  private static final @NonNls String CREATE_PREFIX = "create";
  private static final @NonNls String SET_PREFIX = "set";
  private static final @NonNls String AS_PREFIX = "as";
  private static final @NonNls String TO_PREFIX = "to";

  private static final @NonNls String[] ourPrepositions = {
    "as", "at", "by", "down", "for", "from", "in", "into", "of", "on", "onto", "out", "over",
    "per", "to", "up", "upon", "via", "with"};

  private static final @NonNls String[] ourCommonTypeSuffixes = {"Entity"};

  private final Project myProject;

  public JavaCodeStyleManagerImpl(final Project project) {
    myProject = project;
  }

  @Override
  public @NotNull PsiElement shortenClassReferences(@NotNull PsiElement element) throws IncorrectOperationException {
    return shortenClassReferences(element, 0);
  }

  @Override
  public @NotNull PsiElement shortenClassReferences(@NotNull PsiElement element, int flags) throws IncorrectOperationException {
    CheckUtil.checkWritable(element);
    if (!SourceTreeToPsiMap.hasTreeElement(element)) return element;

    final boolean addImports = !BitUtil.isSet(flags, DO_NOT_ADD_IMPORTS);
    final boolean incompleteCode = BitUtil.isSet(flags, INCOMPLETE_CODE);

    final ReferenceAdjuster adjuster = ReferenceAdjuster.Extension.getReferenceAdjuster(element.getLanguage());
    if (adjuster != null) {
      final ASTNode reference = adjuster.process(element.getNode(), addImports, incompleteCode, myProject);
      return SourceTreeToPsiMap.treeToPsiNotNull(reference);
    }
    else {
      return element;
    }
  }

  @Override
  public void shortenClassReferences(@NotNull PsiElement element, int startOffset, int endOffset) throws IncorrectOperationException {
    CheckUtil.checkWritable(element);
    if (SourceTreeToPsiMap.hasTreeElement(element)) {
      final ReferenceAdjuster adjuster = ReferenceAdjuster.Extension.getReferenceAdjuster(element.getLanguage());
      if (adjuster != null) {
        adjuster.processRange(element.getNode(), startOffset, endOffset, myProject);
      }
    }
  }

  @Override
  public @NotNull PsiElement qualifyClassReferences(@NotNull PsiElement element) {
    final ReferenceAdjuster adjuster = ReferenceAdjuster.Extension.getReferenceAdjuster(element.getLanguage());
    if (adjuster != null) {
      final ASTNode reference = adjuster.process(element.getNode(), false, false, true, true);
      return SourceTreeToPsiMap.treeToPsiNotNull(reference);
    }
    return element;
  }

  @Override
  public void optimizeImports(@NotNull PsiFile file) throws IncorrectOperationException {
    CheckUtil.checkWritable(file);
    if (file instanceof PsiJavaFile && CodeStyle.isFormattingEnabled(file)) {
      PsiImportList newList = prepareOptimizeImportsResult((PsiJavaFile)file);
      if (newList != null) {
        final PsiImportList importList = ((PsiJavaFile)file).getImportList();
        if (importList != null) {
          importList.getParent().addRangeAfter(newList.getParent().getFirstChild(), newList.getParent().getLastChild(), importList);
          importList.delete();
        }
      }
    }
  }

  @Override
  public PsiImportList prepareOptimizeImportsResult(@NotNull PsiJavaFile file) {
    return new ImportHelper(JavaCodeStyleSettings.getInstance(file)).prepareOptimizeImportsResult(file);
  }

  @Override
  public boolean hasConflictingOnDemandImport(@NotNull PsiJavaFile file, @NotNull PsiClass psiClass, @NotNull String referenceName) {
    return ImportHelper.hasConflictingOnStaticDemandImport(file, psiClass, referenceName);
  }

  @Override
  public boolean isStaticAutoImportName(@Nullable String fqn) {
    if (fqn == null) return false;
    JavaProjectCodeInsightSettings settings = JavaProjectCodeInsightSettings.getSettings(myProject);
    return settings.isStaticAutoImportName(fqn);
  }

  @Override
  public boolean addImport(@NotNull PsiJavaFile file, @NotNull PsiClass refClass) {
    return new ImportHelper(JavaCodeStyleSettings.getInstance(file)).addImport(file, refClass);
  }

  @Override
  public void removeRedundantImports(final @NotNull PsiJavaFile file) throws IncorrectOperationException {
    final Collection<PsiImportStatementBase> redundant = findRedundantImports(file);
    if (redundant == null) return;

    for (final PsiImportStatementBase importStatement : redundant) {
      final PsiJavaCodeReferenceElement ref = importStatement.getImportReference();
      //Do not remove non-resolving refs
      if (ref == null || ref.resolve() == null) {
        continue;
      }

      importStatement.delete();
    }
  }

  @Override
  public @Nullable Collection<PsiImportStatementBase> findRedundantImports(final @NotNull PsiJavaFile file) {
    final PsiImportList importList = file.getImportList();
    if (importList == null) return null;
    final PsiImportStatementBase[] imports = importList.getAllImportStatements();
    if (imports.length == 0) return null;

    Set<PsiImportStatementBase> allImports = ContainerUtil.newHashSet(imports);
    final Collection<PsiImportStatementBase> redundant;
    if (FileTypeUtils.isInServerPageFile(file)) {
      // remove only duplicate imports
      redundant = new ReferenceOpenHashSet<>();
      ContainerUtil.addAll(redundant, imports);
      redundant.removeAll(allImports);
      for (PsiImportStatementBase importStatement : imports) {
        if (importStatement instanceof JspxImportStatement && importStatement.isForeignFileImport()) {
          redundant.remove(importStatement);
        }
      }
    }
    else {
      redundant = allImports;
      final List<PsiFile> roots = file.getViewProvider().getAllFiles();
      for (PsiElement root : roots) {
        root.accept(new JavaRecursiveElementWalkingVisitor() {
          @Override
          public void visitReferenceElement(@NotNull PsiJavaCodeReferenceElement reference) {
            if (!reference.isQualified()) {
              final JavaResolveResult resolveResult = reference.advancedResolve(false);
              if (!inTheSamePackage(file, resolveResult.getElement())) {
                final PsiElement resolveScope = resolveResult.getCurrentFileResolveScope();
                if (resolveScope instanceof PsiImportStatementBase importStatementBase) {
                  redundant.remove(importStatementBase);
                }
              }
            }
            super.visitReferenceElement(reference);
          }

          private static boolean inTheSamePackage(PsiJavaFile file, PsiElement element) {
            if (element instanceof PsiClass && ((PsiClass)element).getContainingClass() == null) {
              final PsiFile containingFile = element.getContainingFile();
              return containingFile instanceof PsiClassOwner &&
                     Comparing.strEqual(file.getPackageName(), ((PsiClassOwner)containingFile).getPackageName());
            }
            return false;
          }
        });
      }
    }
    return redundant;
  }

  @Override
  public int findEntryIndex(@NotNull PsiImportStatementBase statement) {
    return new ImportHelper(JavaCodeStyleSettings.getInstance(statement.getContainingFile())).findEntryIndex(statement);
  }

  @Override
  public @Nullable String suggestCompiledParameterName(@NotNull PsiType type) {
    // avoid hang due to nice name evaluation that uses indices for resolve (IDEA-116803)
    Collection<String> result = doSuggestParameterNamesByTypeWithoutIndex(type);
    return ContainerUtil.getFirstItem(getSuggestionsByNames(result, VariableKind.PARAMETER, true, null));
  }

  @Override
  public @NotNull SuggestedNameInfo suggestVariableName(final @NotNull VariableKind kind,
                                                        final @Nullable String propertyName,
                                                        final @Nullable PsiExpression expr,
                                                        @Nullable PsiType type,
                                                        final boolean correctKeywords) {

    if (expr != null && type == null) {
      type = expr.getType();
    }

    Set<String> names = new LinkedHashSet<>();
    if (propertyName != null) {
      String[] namesByName = ArrayUtilRt.toStringArray(getSuggestionsByName(propertyName, kind, correctKeywords, expr));
      sortVariableNameSuggestions(namesByName, kind, propertyName, null);
      ContainerUtil.addAll(names, namesByName);
    }

    final NamesByExprInfo namesByExpr;
    if (expr != null) {
      namesByExpr = suggestVariableNameByExpression(expr, kind);
      String[] suggestions = ArrayUtilRt.toStringArray(getSuggestionsByNames(namesByExpr.names, kind, correctKeywords, expr));
      if (namesByExpr.propertyName != null) {
        sortVariableNameSuggestions(suggestions, kind, namesByExpr.propertyName, null);
      }
      ContainerUtil.addAll(names, suggestions);
    }
    else {
      namesByExpr = null;
    }

    if (type != null) {
      String[] namesByType = suggestVariableNameByType(type, kind, correctKeywords, expr);
      sortVariableNameSuggestions(namesByType, kind, null, type);
      ContainerUtil.addAll(names, namesByType);
    }

    final String _propertyName;
    if (propertyName != null) {
      _propertyName = propertyName;
    }
    else {
      _propertyName = namesByExpr != null ? namesByExpr.propertyName : null;
    }

    filterOutBadNames(names);
    addNamesFromStatistics(names, kind, _propertyName, type);

    String[] namesArray = ArrayUtilRt.toStringArray(names);
    sortVariableNameSuggestions(namesArray, kind, _propertyName, type);

    final String _type = type == null ? null : type.getCanonicalText();
    return new SuggestedNameInfo(namesArray) {
      @Override
      public void nameChosen(String name) {
        if (_propertyName != null || _type != null) {
          JavaStatisticsManager.incVariableNameUseCount(name, kind, _propertyName, _type);
        }
      }
    };
  }

  private static void filterOutBadNames(Set<String> names) {
    names.remove("of");
    names.remove("to");
  }

  private static void addNamesFromStatistics(@NotNull Set<? super String> names,
                                             @NotNull VariableKind variableKind,
                                             @Nullable String propertyName,
                                             @Nullable PsiType type) {
    String[] allNames = JavaStatisticsManager.getAllVariableNamesUsed(variableKind, propertyName, type);

    int maxFrequency = 0;
    for (String name : allNames) {
      int count = JavaStatisticsManager.getVariableNameUseCount(name, variableKind, propertyName, type);
      maxFrequency = Math.max(maxFrequency, count);
    }

    int frequencyLimit = Math.max(5, maxFrequency / 2);

    for (String name : allNames) {
      if (names.contains(name)) {
        continue;
      }
      int count = JavaStatisticsManager.getVariableNameUseCount(name, variableKind, propertyName, type);
      if (LOG.isDebugEnabled()) {
        LOG.debug("new name:" + name + " count:" + count);
        LOG.debug("frequencyLimit:" + frequencyLimit);
      }
      if (count >= frequencyLimit) {
        names.add(name);
      }
    }

    if (propertyName != null && type != null) {
      addNamesFromStatistics(names, variableKind, propertyName, null);
      addNamesFromStatistics(names, variableKind, null, type);
    }
  }

  private String @NotNull [] suggestVariableNameByType(
    @NotNull PsiType type,
    @NotNull VariableKind variableKind,
    boolean correctKeywords,
    @Nullable PsiElement context) {
    Collection<String> byTypeNames = doSuggestNamesByType(type, variableKind);
    return ArrayUtilRt.toStringArray(getSuggestionsByNames(byTypeNames, variableKind, correctKeywords, context));
  }

  private @NotNull Collection<String> doSuggestParameterNamesByTypeWithoutIndex(@NotNull PsiType type) {
    String fromTypeMap = suggestNameFromTypeMap(type, VariableKind.PARAMETER, type.getCanonicalText());
    if (fromTypeMap != null) {
      return Collections.singletonList(fromTypeMap);
    }

    return suggestNamesFromTypeName(type, VariableKind.PARAMETER, getTypeNameWithoutIndex(type.getDeepComponentType()));
  }

  private @NotNull Collection<String> doSuggestNamesByType(@NotNull PsiType type, final @NotNull VariableKind variableKind) {
    String fromTypeMap = suggestNameFromTypeMap(type, variableKind, getLongTypeName(type));
    if (fromTypeMap != null && type instanceof PsiPrimitiveType) {
      return Collections.singletonList(fromTypeMap);
    }
    final Collection<String> suggestions = new LinkedHashSet<>();
    if (fromTypeMap != null) {
      suggestions.add(fromTypeMap);
    }

    List<String> fromTypeName = suggestNamesFromTypeName(type, variableKind, getTypeName(type));
    if (!(type instanceof PsiClassType classType)) {
      suggestions.addAll(fromTypeName);
      return suggestions;
    }

    suggestNamesForCollectionInheritors(classType, suggestions);
    suggestFromOptionalContent(variableKind, classType, suggestions);
    suggestNamesFromGenericParameters(classType, suggestions);
    suggestions.addAll(fromTypeName);
    suggestNamesFromHierarchy(classType, suggestions);
    return suggestions;
  }

  private @Nullable String suggestNameFromTypeMap(
    @NotNull PsiType type,
    @NotNull VariableKind variableKind,
    @Nullable String longTypeName) {
    if (longTypeName != null) {
      if (type.equals(PsiTypes.nullType())) {
        longTypeName = CommonClassNames.JAVA_LANG_OBJECT;
      }
      String name = nameByType(longTypeName, variableKind);
      if (name != null && isIdentifier(name, null)) {
        return type instanceof PsiArrayType ? StringUtil.pluralize(name) : name;
      }
    }
    return null;
  }

  private static @Nullable String nameByType(@NotNull String longTypeName, @NotNull VariableKind kind) {
    if (kind == VariableKind.PARAMETER) {
      return switch (longTypeName) {
        case "int", "boolean", "byte", "char", "long" -> longTypeName.substring(0, 1);
        case "double", "float" -> "v";
        case "short" -> "i";
        case CommonClassNames.JAVA_LANG_OBJECT -> "o";
        case CommonClassNames.JAVA_LANG_STRING -> "s";
        case CommonClassNames.JAVA_LANG_VOID -> "unused";
        default -> null;
      };
    }
    if (kind == VariableKind.LOCAL_VARIABLE) {
      return switch (longTypeName) {
        case "int", "boolean", "byte", "char", "long" -> longTypeName.substring(0, 1);
        case "double", "float", CommonClassNames.JAVA_LANG_DOUBLE, CommonClassNames.JAVA_LANG_FLOAT -> "v";
        case "short", CommonClassNames.JAVA_LANG_SHORT, CommonClassNames.JAVA_LANG_INTEGER -> "i";
        case CommonClassNames.JAVA_LANG_LONG -> "l";
        case CommonClassNames.JAVA_LANG_BOOLEAN, CommonClassNames.JAVA_LANG_BYTE -> "b";
        case CommonClassNames.JAVA_LANG_CHARACTER -> "c";
        case CommonClassNames.JAVA_LANG_OBJECT -> "o";
        case CommonClassNames.JAVA_LANG_STRING -> "s";
        case CommonClassNames.JAVA_LANG_VOID -> "unused";
        default -> null;
      };
    }
    return null;
  }

  private void suggestFromOptionalContent(@NotNull VariableKind variableKind,
                                          @NotNull PsiClassType classType,
                                          @NotNull Collection<? super String> suggestions) {
    final PsiType optionalContent = extractOptionalContent(classType);
    if (optionalContent == null) return;

    final Collection<String> contentSuggestions = doSuggestNamesByType(optionalContent, variableKind);
    suggestions.addAll(contentSuggestions);
    for (String s : contentSuggestions) {
      suggestions.add("optional" + StringUtil.capitalize(s));
    }
  }

  private static @NotNull List<String> suggestNamesFromTypeName(@NotNull PsiType type, @NotNull VariableKind variableKind, @Nullable String typeName) {
    if (typeName == null) return Collections.emptyList();

    typeName = normalizeTypeName(typeName);
    String result = type instanceof PsiArrayType ? StringUtil.pluralize(typeName) : typeName;
    if (variableKind == VariableKind.PARAMETER && type instanceof PsiClassType && typeName.endsWith("Exception")) {
      return Arrays.asList("e", result);
    }
    for (String suffix : ourCommonTypeSuffixes) {
      if (result.length() > suffix.length() && result.endsWith(suffix)) {
        return Arrays.asList(result, result.substring(0, result.length() - suffix.length()));
      }
    }
    return Collections.singletonList(result);
  }

  private static @Nullable PsiType extractOptionalContent(@NotNull PsiClassType classType) {
    final PsiClass resolved = classType.resolve();
    if (resolved != null && CommonClassNames.JAVA_UTIL_OPTIONAL.equals(resolved.getQualifiedName())) {
      if (classType.getParameterCount() == 1) {
        return classType.getParameters()[0];
      }
    }
    return null;
  }

  private static void suggestNamesFromHierarchy(@NotNull PsiClassType type, @NotNull Collection<? super String> suggestions) {
    final PsiClass resolved = type.resolve();
    if (resolved == null || resolved.getContainingClass() == null) return;

    InheritanceUtil.processSupers(resolved, false, superClass -> {
      if (PsiTreeUtil.isAncestor(superClass, resolved, true)) {
        suggestions.add(superClass.getName());
      }
      return false;
    });
  }

  private static void suggestNamesFromGenericParameters(@NotNull PsiClassType type, @NotNull Collection<? super String> suggestions) {
    PsiType[] parameters = type.getParameters();
    if (parameters.length == 0) return;

    StringBuilder fullNameBuilder = new StringBuilder();
    for (PsiType parameter : parameters) {
      if (parameter instanceof PsiClassType) {
        final String typeName = normalizeTypeName(getTypeName(parameter));
        if (typeName != null) {
          fullNameBuilder.append(typeName);
        }
      }
    }
    String baseName = normalizeTypeName(getTypeName(type));
    if (baseName != null) {
      fullNameBuilder.append(baseName);
      suggestions.add(fullNameBuilder.toString());
    }
  }

  private static void suggestNamesForCollectionInheritors(@NotNull PsiClassType type, @NotNull Collection<? super String> suggestions) {
    PsiType componentType = PsiUtil.extractIterableTypeParameter(type, false);
    if (componentType == null || componentType.equals(type)) {
      return;
    }
    String typeName = normalizeTypeName(getTypeName(componentType));
    if (typeName != null) {
      suggestions.add(StringUtil.pluralize(typeName));
    }
  }

  private static String normalizeTypeName(@Nullable String typeName) {
    if (typeName == null) {
      return null;
    }
    if (typeName.endsWith(IMPL_SUFFIX) && typeName.length() > IMPL_SUFFIX.length()) {
      return typeName.substring(0, typeName.length() - IMPL_SUFFIX.length());
    }
    return typeName;
  }

  private static @Nullable String getTypeNameWithoutIndex(@NotNull PsiType type) {
    type = type.getDeepComponentType();
    return type instanceof PsiClassType ? ((PsiClassType)type).getClassName() :
           type instanceof PsiPrimitiveType ? type.getPresentableText() :
           null;
  }

  public static @Nullable String getTypeName(@NotNull PsiType type) {
    type = type.getDeepComponentType();
    if (type instanceof PsiClassType classType) {
      final String className = classType.getClassName();
      if (className != null) return className;
      final PsiClass aClass = classType.resolve();
      return aClass instanceof PsiAnonymousClass ? ((PsiAnonymousClass)aClass).getBaseClassType().getClassName() : null;
    }
    else if (type instanceof PsiPrimitiveType) {
      return type.getPresentableText();
    }
    else if (type instanceof PsiWildcardType) {
      return getTypeName(((PsiWildcardType)type).getExtendsBound());
    }
    else if (type instanceof PsiIntersectionType) {
      return getTypeName(((PsiIntersectionType)type).getRepresentative());
    }
    else if (type instanceof PsiCapturedWildcardType) {
      return getTypeName(((PsiCapturedWildcardType)type).getWildcard());
    }
    else if (type instanceof PsiDisjunctionType) {
      return getTypeName(((PsiDisjunctionType)type).getLeastUpperBound());
    }
    else {
      return null;
    }
  }

  private static @Nullable String getLongTypeName(@NotNull PsiType type) {
    if (type instanceof PsiClassType) {
      PsiClass aClass = ((PsiClassType)type).resolve();
      if (aClass == null) {
        return null;
      }
      else if (aClass instanceof PsiAnonymousClass) {
        PsiClass baseClass = ((PsiAnonymousClass)aClass).getBaseClassType().resolve();
        return baseClass != null ? baseClass.getQualifiedName() : null;
      }
      else {
        return aClass.getQualifiedName();
      }
    }
    else if (type instanceof PsiArrayType) {
      return getLongTypeName(((PsiArrayType)type).getComponentType()) + "[]";
    }
    else if (type instanceof PsiPrimitiveType) {
      return type.getPresentableText();
    }
    else if (type instanceof PsiWildcardType) {
      final PsiType bound = ((PsiWildcardType)type).getBound();
      return bound != null ? getLongTypeName(bound) : CommonClassNames.JAVA_LANG_OBJECT;
    }
    else if (type instanceof PsiCapturedWildcardType) {
      final PsiType bound = ((PsiCapturedWildcardType)type).getWildcard().getBound();
      return bound != null ? getLongTypeName(bound) : CommonClassNames.JAVA_LANG_OBJECT;
    }
    else if (type instanceof PsiIntersectionType) {
      return getLongTypeName(((PsiIntersectionType)type).getRepresentative());
    }
    else if (type instanceof PsiDisjunctionType) {
      return getLongTypeName(((PsiDisjunctionType)type).getLeastUpperBound());
    }
    else {
      return null;
    }
  }

  private static final class NamesByExprInfo {
    static final NamesByExprInfo EMPTY = new NamesByExprInfo(null, Collections.emptyList());

    private final String propertyName;
    private final Collection<String> names;

    private NamesByExprInfo(@Nullable String propertyName, @NotNull Collection<String> names) {
      this.propertyName = propertyName;
      this.names = names;
    }

    private NamesByExprInfo(@NotNull String propertyName) {
      this(propertyName, Collections.singletonList(propertyName));
    }

    private NamesByExprInfo(@Nullable String propertyName, String @NotNull ... names) {
      this(
        propertyName,
        propertyName == null ? Arrays.asList(names) : ContainerUtil.prepend(Arrays.asList(names), propertyName)
      );
    }
  }

  private @NotNull NamesByExprInfo suggestVariableNameByExpression(@NotNull PsiExpression expr, @Nullable VariableKind variableKind) {
    List<String> fromLiteral = ExpressionUtils.nonStructuralChildren(expr)
      .map(e -> e instanceof PsiLiteralExpression literal && literal.getValue() instanceof String str ? str : null)
      .filter(Objects::nonNull)
      .flatMap(literal -> LiteralNameSuggester.literalNames(literal).stream())
      .distinct()
      .toList();
    final LinkedHashSet<String> names = new LinkedHashSet<>(fromLiteral);
    ContainerUtil.addIfNotNull(names, suggestVariableNameFromConstant(expr, variableKind));
    ContainerUtil.addIfNotNull(names, suggestVariableNameFromLiterals(expr));

    NamesByExprInfo byExpr = suggestVariableNameByExpressionOnly(expr, variableKind, false);
    NamesByExprInfo byExprPlace = suggestVariableNameByExpressionPlace(expr, variableKind);
    NamesByExprInfo byExprAllMethods = suggestVariableNameByExpressionOnly(expr, variableKind, true);

    names.addAll(byExpr.names);
    names.addAll(byExprPlace.names);

    PsiType type = expr.getType();
    if (type != null && variableKind != null) {
      names.addAll(doSuggestNamesByType(type, variableKind));
    }
    names.addAll(byExprAllMethods.names);

    String propertyName = byExpr.propertyName != null ? byExpr.propertyName : byExprPlace.propertyName;
    return new NamesByExprInfo(propertyName, names);
  }

  private static @Nullable String suggestVariableNameFromConstant(@NotNull PsiExpression expr, @Nullable VariableKind kind) {
    if (kind == null || kind == VariableKind.LOCAL_VARIABLE) {
      return null;
    }
    PsiExpression expression = PsiUtil.skipParenthesizedExprDown(expr);
    if (expression instanceof PsiReferenceExpression referenceExpression &&
        referenceExpression.resolve() instanceof PsiEnumConstant enumConstant) {
      return normalizeTypeName(getTypeName(enumConstant.getType()));
    }
    return null;
  }

  private static @Nullable String suggestVariableNameFromLiterals(@NotNull PsiExpression expr) {
    String text = findLiteralText(expr);
    if (text == null) return null;
    return expr.getType() instanceof PsiArrayType ? StringUtil.pluralize(text) : text;
  }

  private static boolean isNameSupplier(String text) {
    if (!StringUtil.isQuotedString(text)) return false;
    String stringPresentation = StringUtil.unquoteString(text);
    String[] words = stringPresentation.split(" ");
    if (words.length > 5) return false;
    return ContainerUtil.and(words, StringUtil::isJavaIdentifier);
  }

  private static @Nullable String findLiteralText(@NotNull PsiExpression expr) {
    final PsiLiteralExpression[] literals = SyntaxTraverser.psiTraverser(expr)
      .filter(PsiLiteralExpression.class)
      .filter(lit -> isNameSupplier(lit.getText()))
      .filter(lit -> {
        final PsiElement exprList = lit.getParent();
        if (!(exprList instanceof PsiExpressionList)) return false;
        final PsiElement call = exprList.getParent();
        //TODO: exclude or not getA().getB("name").getC(); or getA(getB("name").getC()); It works fine for now in the most cases
        return call instanceof PsiNewExpression || call instanceof PsiMethodCallExpression;
      })
      .toArray(new PsiLiteralExpression[0]);

    if (literals.length == 1) {
      return StringUtil.unquoteString(literals[0].getText()).replaceAll(" ", "_");
    }
    return null;
  }

  private @NotNull NamesByExprInfo suggestVariableNameByExpressionOnly(@NotNull PsiExpression expr,
                                                                       @Nullable VariableKind variableKind,
                                                                       boolean useAllMethodNames) {
    if (expr instanceof PsiMethodCallExpression call) {
      List<? extends MethodContract> contracts = DumbService.isDumb(call.getProject()) ? 
                                                 List.of() : JavaMethodContractUtil.getMethodCallContracts(call);
      ContractReturnValue returnValue = JavaMethodContractUtil.getNonFailingReturnValue(contracts);
      if (returnValue instanceof ContractReturnValue.ParameterReturnValue parameter) {
        int number = parameter.getParameterNumber();
        PsiExpression[] args = call.getArgumentList().getExpressions();
        if (args.length > number) {
          return suggestVariableNameByExpressionOnly(args[number], variableKind, useAllMethodNames);
        }
      }
      PsiReferenceExpression methodExpr = call.getMethodExpression();
      String methodName = methodExpr.getReferenceName();
      if (methodName != null) {
        if ("of".equals(methodName) || "ofNullable".equals(methodName)) {
          if (isJavaUtilMethodCall(call)) {
            PsiExpression[] expressions = call.getArgumentList().getExpressions();
            if (expressions.length > 0) {
              return suggestVariableNameByExpressionOnly(expressions[0], variableKind, useAllMethodNames);
            }
          }
        }
        if ("map".equals(methodName) || "flatMap".equals(methodName) || "filter".equals(methodName)) {
          if (isJavaUtilMethodCall(call)) {
            return NamesByExprInfo.EMPTY;
          }
        }

        List<@NotNull String> words = NameUtilCore.nameToWordList(methodName);
        if (!words.isEmpty()) {
          final String firstWord = words.getFirst();
          if (GET_PREFIX.equals(firstWord)
              || IS_PREFIX.equals(firstWord)
              || FIND_PREFIX.equals(firstWord)
              || CREATE_PREFIX.equals(firstWord)
              || AS_PREFIX.equals(firstWord)
              || TO_PREFIX.equals(firstWord)) {
            if (words.size() > 1) {
              final String propertyName = methodName.substring(firstWord.length());
              final PsiExpression qualifierExpression = methodExpr.getQualifierExpression();
              if (qualifierExpression instanceof PsiReferenceExpression &&
                  ((PsiReferenceExpression)qualifierExpression).resolve() instanceof PsiVariable) {
                String name = ((PsiReferenceExpression)qualifierExpression).getReferenceName() + StringUtil.capitalize(propertyName);
                return new NamesByExprInfo(propertyName, name);
              }
              return new NamesByExprInfo(propertyName);
            }
          }
          else if (words.size() == 1 || useAllMethodNames) {
            if (Registry.is("add.past.participle.to.suggested.names") && !"equals".equals(firstWord) && !"valueOf".equals(methodName)) {
              String pastParticiple = PastParticiple.pastParticiple(firstWord);
              String name = StreamEx.of(words).mapFirst(__ -> pastParticiple).joining();
              return new NamesByExprInfo(methodName, pastParticiple, name);
            }
            else {
              return new NamesByExprInfo(methodName);
            }
          }
        }
      }
    }
    else if (expr instanceof PsiReferenceExpression) {
      String propertyName = getPropertyName((PsiReferenceExpression)expr, true);
      if (propertyName != null) {
        return new NamesByExprInfo(propertyName);
      }
    }
    else if (expr instanceof PsiArrayAccessExpression) {
      NamesByExprInfo info =
        suggestVariableNameByExpressionOnly(((PsiArrayAccessExpression)expr).getArrayExpression(), variableKind, useAllMethodNames);

      String singular = info.propertyName == null ? null : StringUtil.unpluralize(info.propertyName);
      if (singular != null) {
        return new NamesByExprInfo(singular, ContainerUtil.mapNotNull(info.names, StringUtil::unpluralize));
      }
    }
    else if (expr instanceof PsiLiteralExpression literalExpression && variableKind == VariableKind.STATIC_FINAL_FIELD) {
