// 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.ImportFilter;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.jsp.JspSpiUtil;
import com.intellij.lang.ASTNode;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.Predicates;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.java.JavaFeature;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.JavaCodeStyleSettings;
import com.intellij.psi.codeStyle.PackageEntry;
import com.intellij.psi.codeStyle.PackageEntryTable;
import com.intellij.psi.formatter.java.ImportHelperBase;
import com.intellij.psi.impl.IncompleteModelUtil;
import com.intellij.psi.impl.PsiFileFactoryImpl;
import com.intellij.psi.impl.source.PsiJavaCodeReferenceElementImpl;
import com.intellij.psi.impl.source.SourceTreeToPsiMap;
import com.intellij.psi.impl.source.jsp.jspJava.JspxImportStatement;
import com.intellij.psi.impl.source.resolve.ResolveClassUtil;
import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReference;
import com.intellij.psi.impl.source.tree.ElementType;
import com.intellij.psi.impl.source.tree.JavaJspElementType;
import com.intellij.psi.jsp.JspFile;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.*;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.containers.NotNullList;
import com.siyeh.ig.psiutils.ImportUtils;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public final class ImportHelper extends ImportHelperBase {
  private static final Logger LOG = Logger.getInstance(ImportHelper.class);

  private static final @NonNls String JAVA_LANG_PACKAGE = "java.lang";

  public ImportHelper(@NotNull JavaCodeStyleSettings settings) {
    super(settings);
  }

  /**
   * @deprecated Use {@link #ImportHelper(JavaCodeStyleSettings)} instead. The instance of JavaCodeStyleSettings
   * can be obtained using {@link JavaCodeStyleSettings#getInstance(PsiFile)} method.
   */
  @Deprecated(forRemoval = true)
  public ImportHelper(@NotNull CodeStyleSettings settings) {
    super(settings.getCustomSettings(JavaCodeStyleSettings.class));
  }

  @Nullable("null means no need to replace the import list because they are the same")
  PsiImportList prepareOptimizeImportsResult(@NotNull PsiJavaFile file) {
    return prepareOptimizeImportsResult(file, Predicates.alwaysTrue());
  }

  /**
   * @param filter pretend some references do not exist, so the corresponding imports may be deleted
   * @return the import list to replace with, or null when there's no need to replace the import list because they are the same
   */
  public @Nullable PsiImportList prepareOptimizeImportsResult(@NotNull PsiJavaFile file, @NotNull Predicate<? super Import> filter) {
    PsiImportList oldList = file.getImportList();
    if (oldList == null) return null;

    // Java parser works in a way that comments may be included to the import list, e.g.:
    //     import a;
    //     /* comment */
    //     import b;
    // We want to preserve those comments then.
    List<PsiElement> nonImports = new NotNullList<>();
    // Note: this array may contain "<packageOrClassName>.*" for unresolved imports!
    List<Import> imports =
      collectNamesToImport(file, nonImports)
        .stream()
        .filter(filter)
        .sorted(Comparator.comparing(o -> o.name()))
        .collect(Collectors.toList());

    SortedImportItems items = sortItemsWithModulesAccordingToSettings(imports, mySettings);
    List<Import> resultList = items.imports();
    Map<String, Boolean> classesOrPackagesToImportOnDemand = new HashMap<>();
    List<PsiImportModuleStatement> previousModuleStatements = collectModuleImports(file, mySettings);
    Map<String, PsiImportModuleStatement> moduleStatementMap = collectNamesImportedByModules(file, previousModuleStatements, resultList);
    collectOnDemandImports(resultList, mySettings, classesOrPackagesToImportOnDemand, moduleStatementMap);

    MultiMap<String, String> conflictingMemberNames = new MultiMap<>();
    for (Import anImport : resultList) {
      if (anImport.isStatic()) {
        conflictingMemberNames.putValue(StringUtil.getShortName(anImport.name()), StringUtil.getPackageName(anImport.name()));
      }
    }

    for (String methodName : conflictingMemberNames.keySet()) {
      Collection<String> collection = conflictingMemberNames.get(methodName);
      if (!classesOrPackagesToImportOnDemand.keySet().containsAll(collection)) {
        for (String name : collection) {
          classesOrPackagesToImportOnDemand.remove(name);
        }
      }
    }

    ImportUtils.ImplicitImportChecker checker = ImportUtils.createImplicitImportChecker(file);
    Set<String> classesToUseSingle = findSingleImports(file, resultList, classesOrPackagesToImportOnDemand.keySet(),
                                                       moduleStatementMap.keySet(), checker);

    Collection<PsiImportModuleStatement> moduleImports =
      mySettings.isDeleteUnusedModuleImports() ? moduleStatementMap.values() : previousModuleStatements;

    Set<String> toReimport = calculateOnDemandImportConflicts(file, classesOrPackagesToImportOnDemand, moduleImports,
                                                              moduleStatementMap.keySet());
    classesToUseSingle.addAll(toReimport);

    deleteOnDemandIfAllIsSingle(classesOrPackagesToImportOnDemand, resultList, classesToUseSingle, checker);

    try {
      boolean onDemandFirst = mySettings.isLayoutOnDemandImportFromSamePackageFirst();
      StringBuilder text =
        buildImportListText(resultList,
                            classesOrPackagesToImportOnDemand.keySet(),
                            classesToUseSingle,
                            checker,
                            moduleStatementMap,
                            previousModuleStatements,
                            onDemandFirst,
                            items.moduleIndex());
      for (PsiElement nonImport : nonImports) {
        text.append("\n").append(nonImport.getText());
      }
      String ext = JavaFileType.INSTANCE.getDefaultExtension();
      PsiFileFactory factory = PsiFileFactory.getInstance(file.getProject());
      PsiJavaFile dummyFile = (PsiJavaFile)factory.createFileFromText("_Dummy_." + ext, JavaLanguage.INSTANCE, text, false, false);
      PsiUtil.FILE_LANGUAGE_LEVEL_KEY.set(dummyFile, file.getLanguageLevel());
      PsiFileFactoryImpl.markGenerated(dummyFile);
      CodeStyle.reformatWithFileContext(dummyFile, file);

      PsiImportList newImportList = dummyFile.getImportList();
      assert newImportList != null : dummyFile.getText();
      if (oldList.isReplaceEquivalent(newImportList)) return null;
      return newImportList;
    }
    catch (IncorrectOperationException e) {
      LOG.error(e);
      return null;
    }
  }

  /**
   * Removes on-demand imports from the given map if all corresponding elements are present in the singles set
   * (individually imported) or implicitly imported.
   *
   * @param onDemand a map of package or class names (keys) and boolean values associating them with on-demand imports.
   * @param allList  a list of Import objects representing all current imports in the file.
   * @param singles  a set of individual imports to retain.
   * @param checker  an ImplicitImportChecker used to determine if a name is implicitly imported.
   */
  private static void deleteOnDemandIfAllIsSingle(@NotNull Map<String, Boolean> onDemand,
                                                  @NotNull List<Import> allList,
                                                  @NotNull Set<String> singles,
                                                  @NotNull ImportUtils.ImplicitImportChecker checker) {
    Set<String> save = new HashSet<>();
    if (singles.isEmpty()) return;
    for (Import anImport : allList) {
      String name = anImport.name();
      boolean isStatic = anImport.isStatic();
      String packageOrClassName = StringUtil.getPackageName(name);
      boolean implicitlyImported = checker.isImplicitlyImported(name, isStatic);
      if (!singles.contains(name) && !implicitlyImported && onDemand.containsKey(packageOrClassName)) {
        save.add(packageOrClassName);
      }
    }
    onDemand.keySet().retainAll(save);
  }

  /**
   * Collects the names of classes that are imported by modules specified implicitly in the given Java file and in the import list.
   *
   * @param file       the Java file for which imported class names are being collected.
   * @param statements a list of import module statements that specify the modules from which classes are imported.
   * @param list       a list of import objects representing the imports from which class names need to be collected.
   * @return a map of class names and used module imports.
   */
  private static @NotNull Map<String, PsiImportModuleStatement> collectNamesImportedByModules(@NotNull PsiJavaFile file,
                                                                                              @NotNull List<PsiImportModuleStatement> statements,
                                                                                              @NotNull List<Import> list) {
    List<PsiImportModuleStatement> moduleImports =
      new ArrayList<>(ContainerUtil.filterIsInstance(ImportsUtil.getAllImplicitImports(file), PsiImportModuleStatement.class));
    moduleImports.addAll(statements);

    Map<String, PsiImportModuleStatement> usedClasses = new HashMap<>();
    for (Import anImport : list) {
      if (anImport.isStatic) continue;
      String qualifiedName = anImport.name();
      String referencePackageName = StringUtil.getPackageName(qualifiedName);
      String referenceShortName = StringUtil.getShortName(qualifiedName);
      if (referencePackageName.isEmpty() || referenceShortName.isEmpty()) continue;
      for (PsiImportModuleStatement statement : moduleImports) {
        PsiPackageAccessibilityStatement importedPackage = statement.findImportedPackage(referencePackageName);
        if (importedPackage == null) continue;
        PsiJavaCodeReferenceElement packageReference = importedPackage.getPackageReference();
        if (packageReference == null) continue;
        PsiElement resolved = packageReference.resolve();
        if (!(resolved instanceof PsiPackage psiPackage)) continue;
        if (!psiPackage.containsClassNamed(referenceShortName)) continue;
        usedClasses.put(anImport.name, statement);
      }
    }
    return usedClasses;
  }

  /**
   * Collects the module import statements from the specified Java file, considering the code style settings, to insert in the new import list
   * If there are two modules, one of which (parent) includes another, only this parent will be taken
   *
   * @param file     the Java file from which module import statements are collected.
   * @param settings the code style settings that determine whether module imports should be preserved.
   * @return a list of module import statements present in the Java file.
   */
  private static @NotNull List<PsiImportModuleStatement> collectModuleImports(@NotNull PsiJavaFile file, JavaCodeStyleSettings settings) {
    if (!settings.isPreserveModuleImports()) return Collections.emptyList();
    PsiImportList importList = file.getImportList();
    if (importList == null) return Collections.emptyList();
    return ImportUtils.optimizeModuleImports(file);
  }

  public static void collectOnDemandImports(@NotNull List<Import> resultList,
                                            @NotNull JavaCodeStyleSettings javaCodeStyleSettings,
                                            @NotNull Map<String, Boolean> outClassesOrPackagesToImportOnDemand,
                                            @NotNull Map<String, PsiImportModuleStatement> moduleStatementMap) {
    Object2IntMap<String> packageToCountMap = new Object2IntOpenHashMap<>();
    Object2IntMap<String> classToCountMap = new Object2IntOpenHashMap<>();
    for (Import anImport : resultList) {
      if (!anImport.isStatic() && moduleStatementMap.containsKey(anImport.name)) continue;
      String packageOrClassName = StringUtil.getPackageName(anImport.name());
      if (packageOrClassName.isEmpty()) continue;
      Object2IntMap<String> map = anImport.isStatic() ? classToCountMap : packageToCountMap;
      map.put(packageOrClassName, map.getOrDefault(packageOrClassName, 0) + 1);
    }

    classToCountMap.forEach((className, count) -> {
      if (isToUseImportOnDemand(className, count, true, javaCodeStyleSettings)) {
        outClassesOrPackagesToImportOnDemand.put(className, true);
      }
    });
    packageToCountMap.forEach((packageName, count) -> {
      if (isToUseImportOnDemand(packageName, count, false, javaCodeStyleSettings)) {
        outClassesOrPackagesToImportOnDemand.put(packageName, false);
      }
    });
  }

  private record SortedImportItems(@NotNull List<Import> imports, int moduleIndex) {
  }

  public static @NotNull List<Import> sortItemsAccordingToSettings(@NotNull List<Import> imports,
                                                                   @NotNull JavaCodeStyleSettings settings) {
    return sortItemsWithModulesAccordingToSettings(imports, settings).imports;
  }

  private static @NotNull ImportHelper.SortedImportItems sortItemsWithModulesAccordingToSettings(@NotNull List<Import> imports,
                                                                                                 @NotNull JavaCodeStyleSettings settings) {
    int[] entryForName = ArrayUtil.newIntArray(imports.size());
    PackageEntry[] entries = settings.IMPORT_LAYOUT_TABLE.getEntries();
    int moduleEntryIndex = findEntryIndex("", false, true, entries);
    for (int i = 0; i < imports.size(); i++) {
      Import anImport = imports.get(i);
      entryForName[i] = findEntryIndex(StringUtil.getPackageName(anImport.name()),
                                       settings.LAYOUT_STATIC_IMPORTS_SEPARATELY && anImport.isStatic(), false, entries);
    }

    List<Import> resultList = new ArrayList<>(imports.size());
    int moduleIndex = 0;
    for (int i = 0; i < entries.length; i++) {
      if (i == moduleEntryIndex) {
        moduleIndex = resultList.size();
        continue;
      }
      for (int j = 0; j < imports.size(); j++) {
        if (entryForName[j] == i) {
          resultList.add(imports.get(j));
          imports.set(j, null);
        }
      }
    }
    for (Import name : imports) {
      if (name != null) resultList.add(name);
    }
    return new SortedImportItems(resultList, moduleIndex);
  }

  private static @NotNull Set<String> findSingleImports(@NotNull PsiJavaFile file,
                                                        @NotNull Collection<Import> imports,
                                                        @NotNull Set<String> onDemandImports,
                                                        @NotNull Set<String> namesImportedByModuleStatements,
                                                        @NotNull ImportUtils.ImplicitImportChecker checker) {
    GlobalSearchScope resolveScope = file.getResolveScope();
    String thisPackageName = file.getPackageName();
    JavaPsiFacade facade = JavaPsiFacade.getInstance(file.getProject());

    List<String> onDemandImportsList = new ArrayList<>(onDemandImports);
    List<PsiClass> onDemandElements = ContainerUtil.map(onDemandImportsList, onDemandName -> facade.findClass(onDemandName, resolveScope));
    Set<String> namesToUseSingle = new HashSet<>();
    for (Import anImport : imports) {
      String name = anImport.name();
      String prefix = StringUtil.getPackageName(name);
      if (prefix.isEmpty()) continue;
      boolean isImplicitlyImported = checker.isImplicitlyImported(name, anImport.isStatic());
      if (!onDemandImports.contains(prefix) && !isImplicitlyImported && !namesImportedByModuleStatements.contains(name)) continue;
      String shortName = PsiNameHelper.getShortClassName(name);

      String thisPackageClass = !thisPackageName.isEmpty() ? thisPackageName + "." + shortName : shortName;
      if (facade.findClass(thisPackageClass, resolveScope) != null && facade.findClass(name, resolveScope) != null) {
        namesToUseSingle.add(name);
        continue;
      }
      if (!isImplicitlyImported) {
        String langPackageClass = JAVA_LANG_PACKAGE + "." + shortName; //TODO : JSP!
        if (facade.findClass(langPackageClass, resolveScope) != null && facade.findClass(name, resolveScope) != null) {
          namesToUseSingle.add(name);
          continue;
        }
      }
      PsiResolveHelper resolveHelper = facade.getResolveHelper();

      for (int i = 0; i < onDemandImportsList.size(); i++) {
        String onDemandName = onDemandImportsList.get(i);
        if (prefix.equals(onDemandName)) continue;
        if (anImport.isStatic()) {
          PsiClass aClass = onDemandElements.get(i);
          if (aClass != null) {
            PsiField field = aClass.findFieldByName(shortName, true);
            if (field != null && checkMemberAccessibility(field, resolveHelper, file, aClass, prefix)) {
              namesToUseSingle.add(name);
            }
            else {
              PsiClass inner = aClass.findInnerClassByName(shortName, true);
              if (inner != null && checkMemberAccessibility(inner, resolveHelper, file, aClass, prefix)) {
                namesToUseSingle.add(name);
              }
              else {
                PsiMethod[] methods = aClass.findMethodsByName(shortName, true);
                if (ContainerUtil.exists(methods, psiMethod -> checkMemberAccessibility(psiMethod, resolveHelper, file, aClass, prefix))) {
                  namesToUseSingle.add(name);
                }
              }
            }
          }
        }
        else {
          PsiClass aClass = facade.findClass(onDemandName + "." + shortName, resolveScope);
          if (aClass != null) {
            namesToUseSingle.add(name);
          }
        }
      }
    }

    return namesToUseSingle;
  }

  private static boolean checkMemberAccessibility(@NotNull PsiMember member,
                                                  @NotNull PsiResolveHelper resolveHelper,
                                                  @NotNull PsiFile psiFile,
                                                  @NotNull PsiClass aClass,
                                                  @NotNull @NlsSafe String prefix) {
    if (member.hasModifierProperty(PsiModifier.STATIC) && resolveHelper.isAccessible(member, psiFile, null)) {
      PsiClass containingClass = member.getContainingClass();
      if (containingClass == null) return false;
      for (PsiClass superClass : InheritanceUtil.getSuperClasses(aClass)) {
        if (prefix.equals(superClass.getQualifiedName()) && InheritanceUtil.isInheritorOrSelf(superClass, containingClass, true)) {
          return false;
        }
      }
      return true;
    }
    return false;
  }

  private static Set<String> calculateOnDemandImportConflicts(@NotNull PsiJavaFile file,
                                                              @NotNull Map<String, Boolean> onDemandImports,
                                                              @NotNull Collection<PsiImportModuleStatement> usedModuleStatements,
                                                              @NotNull Set<String> namesImportedByModuleStatements) {
    if (file instanceof PsiCompiledElement) return Collections.emptySet();
    List<PsiImportStatementBase> implicitImports = ImportsUtil.getAllImplicitImports(file);
    List<PsiImportModuleStatement> moduleImports =
      new ArrayList<>(ContainerUtil.filterIsInstance(implicitImports, PsiImportModuleStatement.class));
    moduleImports.addAll(usedModuleStatements);

    List<String> onDemands =
      StreamEx.of(implicitImports)
        .filter(implicit -> implicit.isOnDemand() &&
                            !(implicit instanceof PsiImportModuleStatement) && implicit.getImportReference() != null)
        .map(t -> t.getImportReference().getQualifiedName())
        .toMutableList();
    for (String onDemand : onDemandImports.keySet()) {
      if (!onDemands.contains(onDemand)) {
        onDemands.add(onDemand);
      }
    }
    if (moduleImports.isEmpty() && onDemands.size() < 2) return Collections.emptySet();

    // if we have classes x.A, x.B and there is an "import x.*" then classNames = {"x" -> ("A", "B")}
    GlobalSearchScope resolveScope = file.getResolveScope();
    Map<String, Set<String>> classNames = new HashMap<>();
    JavaPsiFacade facade = JavaPsiFacade.getInstance(file.getProject());
    for (int i = onDemands.size() - 1; i >= 0; i--) {
      String onDemand = onDemands.get(i);
      PsiPackage aPackage = facade.findPackage(onDemand);
      boolean isStatic = ObjectUtils.notNull(onDemandImports.get(onDemand), Boolean.FALSE);
      PsiClass aClass;
      if (aPackage != null) { // import foo.package1.*;
        Set<String> set = Arrays.stream(aPackage.getClasses(resolveScope)).map(PsiClass::getName).collect(Collectors.toSet());
        classNames.put(onDemand, set);
      }
      else if ((aClass = facade.findClass(onDemand, resolveScope)) != null) {  // import static foo.package1.Class1.*;
        if (isStatic) {
          Set<String> set = Arrays.stream(aClass.getInnerClasses())
            .filter(member -> member.hasModifierProperty(PsiModifier.STATIC))
            .map(PsiMember::getName)
            .collect(Collectors.toSet());
          classNames.put(onDemand, set);
        }
        else {
          classNames.put(onDemand, Arrays.stream(aClass.getInnerClasses()).map(PsiClass::getName).collect(Collectors.toSet()));
        }
      }
      else {
        onDemands.remove(i);
      }
    }

    Set<String> conflicts = new HashSet<>();
    for (int i = 0; i < onDemands.size(); i++) {
      String on1 = onDemands.get(i);
      for (int j = i + 1; j < onDemands.size(); j++) {
        String on2 = onDemands.get(j);
        Set<String> intersection = new HashSet<>(classNames.get(on1));
        intersection.retainAll(classNames.get(on2));

        conflicts.addAll(intersection);
      }
    }
    if (moduleImports.isEmpty() && conflicts.isEmpty()) return Collections.emptySet();

    Set<String> result = new HashSet<>();
    String filePackageName = file.getPackageName();
    ImportUtils.ImplicitImportChecker checker = ImportUtils.createImplicitImportChecker(file);
    for (PsiClass aClass : file.getClasses()) {
      // do not visit imports
      aClass.accept(new JavaRecursiveElementVisitor() {
        @Override
        public void visitReferenceElement(@NotNull PsiJavaCodeReferenceElement reference) {
          super.visitReferenceElement(reference);
          if (reference.getQualifier() != null) return;
          JavaResolveResult resolveResult = reference.advancedResolve(false);
          if (!(resolveResult.getElement() instanceof PsiClass psiClass)) return;
          String qualifiedName = psiClass.getQualifiedName();
          if (qualifiedName == null) return;
          String referencePackageName = StringUtil.getPackageName(qualifiedName);
          String referenceShortName = StringUtil.getShortName(qualifiedName);
          //conflict with packages
          boolean hasConflict = conflicts.contains(psiClass.getName());
          if (!hasConflict) {
            //if it is already imported by on demands, there is no conflict with modules
            if (!(PsiUtil.isAvailable(JavaFeature.PACKAGE_IMPORTS_SHADOW_MODULE_IMPORTS, file) &&
                  classNames.getOrDefault(referencePackageName, Collections.emptySet()).contains(referenceShortName)) ||
                //with PACKAGE_IMPORTS_SHADOW_MODULE_IMPORTS conflicts are possible only between modules, other types of imports should be more important
                namesImportedByModuleStatements.contains(qualifiedName)) {
              hasConflict = !moduleImports.isEmpty() &&
                            ImportUtils.hasOnDemandImportConflictWithImports(file, moduleImports, qualifiedName, false, true);
            }
          }
          if (!hasConflict) return;
          //can be visible by inheritance
          if (!(resolveResult.getCurrentFileResolveScope() instanceof PsiImportStatementBase) &&
              !isImplicitlyImported(psiClass, checker)) {
            return;
          }
          //in the same package or in the same class
          if (PsiTreeUtil.isAncestor(file, psiClass, true) ||
              filePackageName.equals(referencePackageName)) {
            return;
          }
          result.add(qualifiedName);
        }
      });
    }
    return result;
  }

  private @NotNull StringBuilder buildImportListText(@NotNull List<Import> imports,
                                                     @NotNull Set<String> packagesOrClassesToImportOnDemand,
                                                     @NotNull Set<String> namesToUseSingle,
                                                     @NotNull ImportUtils.ImplicitImportChecker implicitImportContext,
                                                     @NotNull Map<String, PsiImportModuleStatement> moduleStatementMap,
                                                     @NotNull List<PsiImportModuleStatement> previousModuleStatements,
                                                     boolean onDemandImportsFirst,
                                                     int moduleIndex) {
    Set<String> importedPackagesOrClasses = new HashSet<>();
    @NonNls StringBuilder buffer = new StringBuilder();

    Set<PsiImportModuleStatement> usedModuleImports = new HashSet<>();
    int indexModuleString = -1;
    for (int i = 0; i < imports.size(); i++) {
      if (i == moduleIndex) {
        indexModuleString = buffer.length();
      }
      Import importedName = imports.get(i);
      String name = importedName.name();
      boolean isStatic = importedName.isStatic();
      String packageOrClassName = StringUtil.getPackageName(name);
      boolean implicitlyImported = implicitImportContext.isImplicitlyImported(name, isStatic);
      if (!implicitlyImported && packagesOrClassesToImportOnDemand.remove(packageOrClassName)) {
        appendImportStatement(packageOrClassName + ".*", isStatic, buffer);
        importedPackagesOrClasses.add(packageOrClassName);
      }
      if (namesToUseSingle.contains(name)) {
        if (!implicitlyImported && !onDemandImportsFirst && importedPackagesOrClasses.contains(packageOrClassName)) {
          buffer.insert(buffer.lastIndexOf("import "), "import " + (isStatic ? "static " : "") + name + ";\n");
        }
        else {
          appendImportStatement(name, isStatic, buffer);
        }
      }
      else if (!implicitlyImported && !importedPackagesOrClasses.contains(packageOrClassName)) {
        if (!moduleStatementMap.containsKey(name)) {
          appendImportStatement(name, isStatic, buffer);
        }
        else {
          usedModuleImports.add(moduleStatementMap.get(name));
        }
      }
    }

    if (!mySettings.isDeleteUnusedModuleImports()) {
      usedModuleImports = new HashSet<>(previousModuleStatements);
    }
    StringBuilder moduleStatements = new StringBuilder();
    usedModuleImports.stream()
      .sorted(Comparator.comparing(m -> {
        PsiJavaModuleReferenceElement reference = m.getModuleReference();
        if (reference == null) return "";
        return reference.getText();
      }))
      .map(m -> m.getText())
      .forEach(importModuleStatement -> {
        moduleStatements.append(importModuleStatement).append("\n");
      });
    if (!moduleStatements.isEmpty()) {
      if (indexModuleString != -1) {
        buffer.insert(indexModuleString, moduleStatements);
      }
      else {
        buffer.append(moduleStatements);
      }
    }

    return buffer;
  }

  private static void appendImportStatement(String name, boolean isStatic, StringBuilder buffer) {
    buffer.append("import ");
    if (isStatic) buffer.append("static ");
    buffer.append(name).append(";\n");
  }

  /**
   * Adds import if it is needed.
   *
   * @return false when the FQN has to be used in code (e.g. when conflicting imports already exist)
   */
  public boolean addImport(@NotNull PsiJavaFile file, @NotNull PsiClass refClass) {
    return addImport(file, refClass, false);
  }

  private boolean addImport(@NotNull PsiJavaFile file, @NotNull PsiClass refClass, boolean forceReimport) {
    JavaPsiFacade facade = JavaPsiFacade.getInstance(file.getProject());
    PsiElementFactory factory = facade.getElementFactory();
    PsiResolveHelper helper = facade.getResolveHelper();

    String className = refClass.getQualifiedName();
    if (className == null) return true;

    if (!ImportFilter.shouldImport(file, className)) {
      return false;
    }
    String packageName = StringUtil.getPackageName(className);
    String shortName = PsiNameHelper.getShortClassName(className);

    PsiImportStatement unusedSingleImport = findUnusedSingleImport(file, shortName, className);
    if (unusedSingleImport != null) {
      unusedSingleImport.delete();
    }

    if (!forceReimport) {
      PsiClass conflictSingleRef = findSingleImportByShortName(file, shortName);
      if (conflictSingleRef != null) {
        return className.equals(conflictSingleRef.getQualifiedName());
      }
    }

    PsiClass curRefClass = helper.resolveReferencedClass(shortName, file);
    if (file.getManager().areElementsEquivalent(refClass, curRefClass)) {
      return true;
    }

    boolean useOnDemand = !packageName.isEmpty();

    //right now supports import on demand only for packages
    //it doesn't support add imports on module
    if (hasImportOnDemand(file, packageName)) {
      useOnDemand = false;
    }

    List<PsiClass> classesToReimport = new ArrayList<>();

    List<PsiJavaCodeReferenceElement> importRefs = getImportsFromPackage(file, packageName);
    if (useOnDemand) {
      if (mySettings.USE_SINGLE_CLASS_IMPORTS &&
          importRefs.size() + 1 < mySettings.CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND &&
          !mySettings.PACKAGES_TO_USE_IMPORT_ON_DEMAND.contains(packageName)) {
        useOnDemand = false;
      }
      // the name of class we try to import is the same as of the class defined in this package
      if (curRefClass != null) {
        useOnDemand = true;
      }
      // check conflicts
      if (useOnDemand) {
        calcNonStaticClassesToReimport(file, facade, helper, packageName, classesToReimport);
      }
    }

    if (useOnDemand &&
        refClass.getContainingClass() != null &&
        mySettings.isInsertInnerClassImportsFor(PsiNameHelper.getShortClassName(className)) &&
        containsInCurrentPackage(file, curRefClass)) {
      return false;
    }

    if (curRefClass != null) {
      if (!classesToReimport.isEmpty()) {
        return false;
      }
      else {
        useOnDemand = false;
      }
    }

    try {
      PsiImportList importList = file.getImportList();
      assert importList != null : file;
      PsiImportStatement statement =
        useOnDemand ? factory.createImportStatementOnDemand(packageName) : factory.createImportStatement(refClass);
      importList.add(statement);
      if (useOnDemand) {
        for (PsiJavaCodeReferenceElement ref : importRefs) {
          if (ref.getParent() instanceof PsiImportStatement importStatement) {
            if (!ref.isValid()) continue; // todo[dsl] Q?
            if (importStatement.isForeignFileImport()) {
              // we should not optimize imports from include files
              continue;
            }
