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

import com.intellij.build.events.MessageEvent;
import com.intellij.build.issue.BuildIssue;
import com.intellij.buildsystem.model.unified.UnifiedCoordinates;
import com.intellij.gradle.toolingExtension.util.GradleVersionUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.model.ProjectKeys;
import com.intellij.openapi.externalSystem.model.project.*;
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleType;
import com.intellij.openapi.module.ModuleTypeManager;
import com.intellij.openapi.roots.DependencyScope;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.util.text.Strings;
import com.intellij.platform.workspace.jps.entities.ModuleId;
import com.intellij.platform.workspace.storage.EntityStorage;
import com.intellij.util.ConcurrencyUtil;
import com.intellij.util.PathUtilRt;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap;
import org.gradle.api.artifacts.Dependency;
import org.gradle.tooling.model.BuildIdentifier;
import org.gradle.tooling.model.GradleProject;
import org.gradle.tooling.model.idea.IdeaModule;
import org.jetbrains.annotations.*;
import org.jetbrains.plugins.gradle.DefaultExternalDependencyId;
import org.jetbrains.plugins.gradle.ExternalDependencyId;
import org.jetbrains.plugins.gradle.issue.UnresolvedDependencySyncIssue;
import org.jetbrains.plugins.gradle.model.*;
import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData;
import org.jetbrains.plugins.gradle.service.cache.GradleLocalCacheHelper;
import org.jetbrains.plugins.gradle.settings.GradleExecutionWorkspace;
import org.jetbrains.plugins.gradle.util.GradleConstants;
import org.jetbrains.plugins.gradle.util.GradleModuleDataKt;
import org.jetbrains.plugins.gradle.util.GradleUtil;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.intellij.openapi.util.NullUtils.hasNull;
import static org.jetbrains.plugins.gradle.service.cache.GradleLocalCacheHelper.findArtifactComponents;

/**
 * @author Vladislav.Soroka
 */
public final class GradleProjectResolverUtil {
  private static final Logger LOG = Logger.getInstance(GradleProjectResolverUtil.class);
  private static final @NonNls String SOURCE_JAR_SUFFIX = "-sources.jar";
  private static final @NonNls String JAVADOC_JAR_SUFFIX = "-javadoc.jar";

  public static @NotNull DataNode<ModuleData> createMainModule(@NotNull ProjectResolverContext resolverCtx,
                                                               @NotNull IdeaModule gradleModule,
                                                               @NotNull DataNode<ProjectData> projectDataNode) {
    GradleProject gradleProject = gradleModule.getGradleProject();
    final String moduleName = resolverCtx.isUseQualifiedModuleNames()
                              ? gradleProject.getName()
                              : gradleModule.getName();

    if (moduleName == null) {
      throw new IllegalStateException("Module with undefined name detected: " + gradleModule);
    }

    final ProjectData projectData = projectDataNode.getData();
    final String mainModuleConfigPath = getModuleConfigPath(resolverCtx, gradleModule, projectData.getLinkedExternalProjectPath());
    final String ideProjectPath = resolverCtx.getIdeProjectPath();
    final String relativePath;
    boolean isUnderProjectRoot = FileUtil.isAncestor(projectData.getLinkedExternalProjectPath(), mainModuleConfigPath, false);
    if (isUnderProjectRoot) {
      relativePath = FileUtil.getRelativePath(projectData.getLinkedExternalProjectPath(), mainModuleConfigPath, '/');
    }
    else {
      relativePath = String.valueOf(FileUtil.pathHashCode(mainModuleConfigPath));
    }
    final String mainModuleFileDirectoryPath =
      ideProjectPath == null
      ? mainModuleConfigPath
      : ideProjectPath + '/' + (relativePath == null || relativePath.equals(".") ? "" : relativePath);
    if (GradleProjectResolver.DEBUG_ORPHAN_MODULES_PROCESSING) {
      LOG.info(String.format(
        "Creating module data ('%s') with the external config path: '%s'", gradleProject.getPath(), mainModuleConfigPath
      ));
    }

    String mainModuleId = getModuleId(resolverCtx, gradleModule);
    final ModuleData moduleData =
      new ModuleData(mainModuleId, GradleConstants.SYSTEM_ID, getDefaultModuleTypeId(), moduleName,
                     mainModuleFileDirectoryPath, mainModuleConfigPath);

    ExternalProject externalProject = resolverCtx.getExtraProject(gradleModule, ExternalProject.class);
    if (externalProject != null) {
      GradleModuleDataKt.setGradlePath(moduleData, externalProject.getPath());
      GradleModuleDataKt.setGradleIdentityPath(moduleData, getIdentityPath(resolverCtx, externalProject, projectData.getExternalName()));
      moduleData.setInternalName(getInternalModuleName(gradleModule, externalProject, resolverCtx));
      moduleData.setGroup(externalProject.getGroup());
      moduleData.setVersion(externalProject.getVersion());
      moduleData.setDescription(externalProject.getDescription());
      moduleData.setModuleName(moduleName);
      if (!resolverCtx.isResolveModulePerSourceSet()) {
        moduleData.setArtifacts(externalProject.getArtifacts());
        moduleData.setPublication(new ProjectId(externalProject.getGroup(),
                                                externalProject.getName(),
                                                externalProject.getVersion()));
      }
    }

    File rootDir = gradleProject.getProjectIdentifier().getBuildIdentifier().getRootDir();
    String rootProjectPath = ExternalSystemApiUtil.toCanonicalPath(rootDir.getPath());
    boolean isComposite = !resolverCtx.getNestedBuilds().isEmpty();
    boolean isIncludedBuildTaskRunningSupported = isComposite && isIncludedBuildTaskRunningSupported(projectDataNode, resolverCtx);
    File mainBuildRootDir = resolverCtx.getRootBuild().getBuildIdentifier().getRootDir();
    String mainBuildRootPath = ExternalSystemApiUtil.toCanonicalPath(mainBuildRootDir.getPath());
    boolean isFromIncludedBuild = !rootProjectPath.equals(mainBuildRootPath);

    boolean useIncludedBuildPathPrefix = isFromIncludedBuild && isIncludedBuildTaskRunningSupported;
    String compositeBuildGradlePath = useIncludedBuildPathPrefix ? ":" + getRootProject(gradleProject).getName() : "";
    GradleModuleDataKt.setIncludedBuild(moduleData, isFromIncludedBuild);

    String directoryToRunTask;
    if (compositeBuildGradlePath.isEmpty()) {
      directoryToRunTask = rootProjectPath;
    }
    else {
      directoryToRunTask = mainBuildRootPath;
    }
    GradleModuleDataKt.setDirectoryToRunTask(moduleData, directoryToRunTask);
    return projectDataNode.createChild(ProjectKeys.MODULE, moduleData);
  }

  private static @NotNull String getIdentityPath(@NotNull ProjectResolverContext resolverCtx,
                                                 @NotNull ExternalProject externalProject,
                                                 @NotNull @NlsSafe String rootProjectName) {
    String buildSrcPrefix = getBuildSrcPrefix(resolverCtx, rootProjectName);
    if (!StringUtil.isEmpty(buildSrcPrefix)) {
      return patchBuildSrcIdentity(buildSrcPrefix, externalProject.getIdentityPath());
    }
    return externalProject.getIdentityPath();
  }

  private static @Nullable String getBuildSrcPrefix(@NotNull ProjectResolverContext ctx, @NotNull String rootProjectName) {
    if (ctx.getBuildSrcGroup() == null) {
      return null;
    }
    return ":" + ctx.getBuildSrcGroup() + ":buildSrc";
  }

  private static String patchBuildSrcIdentity(@NotNull String prefix, @NotNull String path) {
    return path.isEmpty() || path.equals(":") ? prefix : prefix + path;
  }


  private static @NotNull GradleProject getRootProject(@NotNull GradleProject project) {
    GradleProject result = project;
    while (result.getParent() != null) {
      result = result.getParent();
    }
    return result;
  }

  private static final Key<Boolean> IS_INCLUDED_BUILD_TASK_RUN_SUPPORTED = Key.create("is included build task running supported");
  private static boolean isIncludedBuildTaskRunningSupported(@NotNull DataNode<ProjectData> project,
                                                             @NotNull ProjectResolverContext resolverCtx) {
    Boolean isSupported = IS_INCLUDED_BUILD_TASK_RUN_SUPPORTED.get(project);
    if (isSupported == null) {
      isSupported = GradleVersionUtil.isGradleAtLeast(resolverCtx.getGradleVersion(), "6.8");
      IS_INCLUDED_BUILD_TASK_RUN_SUPPORTED.set(project, isSupported);
    }
    return isSupported;
  }

  public static String getDefaultModuleTypeId() {
    ModuleType moduleType = ModuleTypeManager.getInstance().getDefaultModuleType();
    return moduleType.getId();
  }

  static @NotNull String getInternalModuleName(@NotNull IdeaModule gradleModule, @NotNull ExternalProject externalProject,
                                               @NotNull ProjectResolverContext resolverCtx) {
    return getInternalModuleName(gradleModule, externalProject, null, resolverCtx);
  }

  static @NotNull String getInternalModuleName(@NotNull IdeaModule gradleModule,
                                               @NotNull ExternalProject externalProject,
                                               @Nullable String sourceSetName,
                                               @NotNull ProjectResolverContext resolverCtx) {
    String delimiter;
    StringBuilder moduleName = new StringBuilder();
    String rootName = gradleModule.getProject().getName();
    BuildIdentifier buildIdentifier = gradleModule.getGradleProject().getProjectIdentifier().getBuildIdentifier();
    String buildSrcGroup = resolverCtx.getBuildSrcGroup(rootName, buildIdentifier);
    if (resolverCtx.isUseQualifiedModuleNames()) {
      delimiter = ".";
      if (StringUtil.isNotEmpty(buildSrcGroup)) {
        moduleName.append(buildSrcGroup).append(delimiter);
      }
      moduleName.append(gradlePathToQualifiedName(rootName, externalProject.getQName()));
    }
    else {
      delimiter = "_";
      if (StringUtil.isNotEmpty(buildSrcGroup)) {
        moduleName.append(buildSrcGroup).append(delimiter);
      }
      moduleName.append(gradleModule.getName());
    }
    if (sourceSetName != null) {
      assert !sourceSetName.isEmpty();
      moduleName.append(delimiter);
      moduleName.append(sourceSetName);
    }
    return PathUtilRt.suggestFileName(moduleName.toString(), true, false);
  }

  /**
   * The Gradle holder module is module that corresponds to the Gradle project and holds the Gradle source set modules.
   *
   * @param context         the unified container for sync settings, parameters, models, etc.
   * @param projectModel    defines Gradle project (build.gradle[.kts])
   * @param externalProject a helper model with the Gradle project identity path
   * @return the Gradle holder module name
   */
  @ApiStatus.Internal
  public static @NotNull String getHolderModuleName(
    @NotNull ProjectResolverContext context,
    @NotNull GradleLightProject projectModel,
    // TODO: replace with GradleLightProject#identityPath
    @NotNull ExternalProject externalProject
  ) {
    var rootBuildPath = context.getRootBuild().getBuildIdentifier().getRootDir().toPath();
    var rootBuildName = context.getRootBuild().getName();
    var buildPath = projectModel.getBuild().getBuildIdentifier().getRootDir().toPath();
    var buildName = projectModel.getBuild().getName();
    var identityPath = externalProject.getIdentityPath();

    var moduleName = Strings.trimStart(identityPath, ":");

    var isRootBuild = rootBuildPath.equals(buildPath);
    var isBuildSrcOfRootBuild = rootBuildPath.equals(buildPath.getParent()) &&
                                buildName.equals(GradleConstants.BUILD_SRC_NAME);
    if (isRootBuild || isBuildSrcOfRootBuild) {
      moduleName = rootBuildName + ":" + moduleName;
    }
    if (context.getBuildSrcGroup() != null) {
      moduleName = context.getBuildSrcGroup() + ":" + moduleName;
    }

    // if identityPath is ":"
    moduleName = Strings.trimEnd(moduleName, ":");

    return Arrays.stream(moduleName.split(":"))
      .map(it -> escapeModuleNameElement(it))
      .collect(Collectors.joining("."));
  }

  /**
   * The Gradle holder module is module that corresponds to the Gradle project and holds the Gradle source set modules.
   *
   * @param context         the unified container for sync settings, parameters, models, etc.
   * @param projectModel    defines Gradle project (build.gradle[.kts])
   * @param externalProject a helper model with the Gradle project identity path
   * @param sourceSetName   the Gradle source set name
   * @return the Gradle source set module name
   */
  @ApiStatus.Internal
  public static @NotNull String resolveSourceSetModuleName(
    @NotNull ProjectResolverContext context,
    @NotNull EntityStorage storage,
    @NotNull GradleLightProject projectModel,
    // TODO: replace with GradleLightProject#identityPath
    @NotNull ExternalProject externalProject,
    @NotNull String sourceSetName
  ) {
    var holderModuleName = getHolderModuleName(context, projectModel, externalProject);
    var sourceSetModuleName = holderModuleName + "." + escapeModuleNameElement(sourceSetName);

    if (storage.contains(new ModuleId(sourceSetModuleName))) {
      // Related issue: IDEA-169388
      // The user can create the 'test' holder module that conflicts with the `test` source set module.
      return sourceSetModuleName + "~1";
    }

    return sourceSetModuleName;
  }

  /**
   * Escapes forbidden elements in the module name's element.
   * The fully qualified module name consists from module group's elements and module name.
   */
  private static @NotNull String escapeModuleNameElement(
    @NotNull String moduleNameElement
  ) {
    return moduleNameElement
      .replace(" ", "_")
      .replace("/", "_")
      .replace("\\\\", "_")
      .replace(".", "_");
  }

  private static @NotNull String gradlePathToQualifiedName(@NotNull String rootName,
                                                           @NotNull String gradlePath) {
    return
      (gradlePath.startsWith(":") ? rootName + "." : "")
      + Arrays.stream(gradlePath.split(":"))
        .filter(s -> !s.isEmpty())
        .collect(Collectors.joining("."));
  }

  public static @NotNull String getModuleConfigPath(@NotNull ProjectResolverContext resolverCtx,
                                                    @NotNull IdeaModule gradleModule,
                                                    @NotNull String rootProjectPath) {
    ExternalProject externalProject = resolverCtx.getExtraProject(gradleModule, ExternalProject.class);
    if (externalProject != null) {
      File projectDir = externalProject.getProjectDir();
      return ExternalSystemApiUtil.toCanonicalPath(projectDir.getPath());
    }
    return GradleUtil.getConfigPath(gradleModule.getGradleProject(), rootProjectPath);
  }

  public static @NotNull String getModuleId(
    @NotNull ProjectResolverContext context,
    @NotNull IdeaModule gradleModule
  ) {
    ExternalProject externalProject = getExternalProject(context, gradleModule);
    return getModuleId(context, externalProject);
  }

  public static @NotNull String getModuleId(
    @NotNull ProjectResolverContext context,
    @NotNull IdeaModule gradleModule,
    @NotNull ExternalSourceSet sourceSet
  ) {
    ExternalProject externalProject = getExternalProject(context, gradleModule);
    return getModuleId(context, externalProject, sourceSet);
  }

  private static @NotNull ExternalProject getExternalProject(
    @NotNull ProjectResolverContext context,
    @NotNull IdeaModule gradleModule
  ) {
    ExternalProject externalProject = context.getProjectModel(gradleModule, ExternalProject.class);
    if (externalProject == null) {
      throw new IllegalStateException(
        "Missing " + ExternalProject.class.getSimpleName() + " for " + gradleModule.getProjectIdentifier().getProjectPath()
      );
    }
    return externalProject;
  }

  public static @NotNull String getModuleId(
    @NotNull ProjectResolverContext context,
    @NotNull ExternalProject externalProject,
    @NotNull ExternalSourceSet sourceSet
  ) {
    String mainModuleId = getModuleId(context, externalProject);
    return mainModuleId + ":" + sourceSet.getName();
  }

  public static @NotNull String getModuleId(
    @NotNull ProjectResolverContext context,
    @NotNull ExternalProject externalProject
  ) {
    if (!StringUtil.isEmpty(context.getBuildSrcGroup())) {
      return context.getBuildSrcGroup() + ":" + getModuleId(externalProject);
    }
    return getModuleId(externalProject);
  }

  private static @NotNull String getModuleId(
    @NotNull ExternalProject externalProject
  ) {
    String moduleName = externalProject.getName();
    String gradlePath = externalProject.getIdentityPath();
    if (StringUtil.isEmpty(gradlePath)) {
      return moduleName;
    }
    if (":".equals(gradlePath)) {
      return moduleName;
    }
    return gradlePath;
  }

  @TestOnly
  public static @NotNull String getModuleId(
    @NotNull ExternalProject externalProject,
    @NotNull ExternalSourceSet sourceSet
  ) {
    String mainModuleId = getModuleId(externalProject);
    return mainModuleId + ":" + sourceSet.getName();
  }

  public static @NotNull String getModuleId(@NotNull ExternalProjectDependency projectDependency) {
    DependencyScope dependencyScope = getDependencyScope(projectDependency.getScope());
    String projectPath = projectDependency.getProjectPath();
    String moduleId = StringUtil.isEmpty(projectPath) || ":".equals(projectPath) ? projectDependency.getName() : projectPath;
    if (Dependency.DEFAULT_CONFIGURATION.equals(projectDependency.getConfigurationName())) {
      moduleId += dependencyScope == DependencyScope.TEST ? ":test" : ":main";
    }
    else {
      moduleId += (':' + projectDependency.getConfigurationName());
    }
    return moduleId;
  }

  public static @NotNull String getModuleId(
    @NotNull ProjectResolverContext context,
    @NotNull GradleLightProject projectModel
  ) {
    if (!StringUtil.isEmpty(context.getBuildSrcGroup())) {
      return context.getBuildSrcGroup() + ":" + getModuleId(projectModel);
    }
    return getModuleId(projectModel);
  }

  private static @NotNull String getModuleId(
    @NotNull GradleLightProject projectModel
  ) {
    String identityPath = projectModel.getIdentityPath();
    if (":".equals(identityPath)) {
      return projectModel.getName();
    }
    return identityPath;
  }

  public static @Nullable String getSourceSetName(final Module module) {
    if (!ExternalSystemApiUtil.isExternalSystemAwareModule(GradleConstants.SYSTEM_ID, module)) return null;
    if (!GradleConstants.GRADLE_SOURCE_SET_MODULE_TYPE_KEY.equals(ExternalSystemApiUtil.getExternalModuleType(module))) return null;

    String externalProjectId = ExternalSystemApiUtil.getExternalProjectId(module);
    if (externalProjectId == null) return null;
    int i = externalProjectId.lastIndexOf(':');
    if (i == -1 || externalProjectId.length() < i + 1) return null;

    return externalProjectId.substring(i + 1);
  }

  public static @Nullable String getGradleIdentityPathOrNull(final Module module) {
    if (!ExternalSystemApiUtil.isExternalSystemAwareModule(GradleConstants.SYSTEM_ID, module)) return null;
    final String externalProjectId = ExternalSystemApiUtil.getExternalProjectId(module);
    if (externalProjectId == null) return null;

    final String moduleType = ExternalSystemApiUtil.getExternalModuleType(module);
    boolean trimSourceSet = GradleConstants.GRADLE_SOURCE_SET_MODULE_TYPE_KEY.equals(moduleType);
    List<String> pathParts = StringUtil.split(externalProjectId, ":");
    if (!externalProjectId.startsWith(":") && !pathParts.isEmpty()) {
      pathParts = pathParts.subList(1, pathParts.size());
    }
    if (trimSourceSet && !pathParts.isEmpty()) {
      pathParts = pathParts.subList(0, pathParts.size() - 1);
    }
    String join = StringUtil.join(pathParts, ":");
    return join.isEmpty() ? ":" : ":" + join;
  }


  public static @NotNull DependencyScope getDependencyScope(@Nullable String scope) {
    return scope != null ? DependencyScope.valueOf(scope) : DependencyScope.COMPILE;
  }

  private static @NotNull DependencyScope getMergedDependencyScope(@Nullable String scope1, @Nullable String scope2) {
    return DependencyScope.coveringUseCasesOf(getDependencyScope(scope1), getDependencyScope(scope2));
  }

  public static void attachGradleSdkSources(final @NotNull IdeaModule gradleModule,
                                            final @Nullable File libFile,
                                            final @NotNull LibraryData library,
                                            final @NotNull ProjectResolverContext resolverCtx) {
    if (libFile == null || !libFile.getName().startsWith("gradle-")) return;
    final GradleBuildScriptClasspathModel buildScriptClasspathModel =
      resolverCtx.getExtraProject(gradleModule, GradleBuildScriptClasspathModel.class);
    if (buildScriptClasspathModel == null) return;
    final File gradleHomeDir = buildScriptClasspathModel.getGradleHomeDir();
    if (gradleHomeDir == null) return;
    final String gradleVersion = buildScriptClasspathModel.getGradleVersion();
    attachGradleSdkSources(libFile, library, gradleHomeDir, gradleVersion);
  }

  public static void attachGradleSdkSources(final @Nullable File libFile,
                                            final @NotNull LibraryData library,
                                            final @NotNull File gradleHomeDir,
                                            final @NotNull String gradleVersion) {
    if (libFile == null || !libFile.getName().startsWith("gradle-")) return;
    if (!FileUtil.isAncestor(gradleHomeDir, libFile, true)) {
      File libFileParent = libFile.getParentFile();
      if (libFileParent == null || !StringUtil.equals("generated-gradle-jars", libFileParent.getName())) return;
      if (("gradle-api-" + gradleVersion + ".jar").equals(libFile.getName())) {
        File gradleSrc = new File(gradleHomeDir, "src");
        File[] gradleSrcRoots = gradleSrc.listFiles();
        if (gradleSrcRoots == null) return;
        for (File srcRoot : gradleSrcRoots) {
          library.addPath(LibraryPathType.SOURCE, srcRoot.getPath());
        }
      }
      return;
    }

    File libOrPluginsFile = libFile.getParentFile();
    if (libOrPluginsFile != null && ("plugins".equals(libOrPluginsFile.getName()))) {
      libOrPluginsFile = libOrPluginsFile.getParentFile();
    }

    if (libOrPluginsFile != null && "lib".equals(libOrPluginsFile.getName()) && libOrPluginsFile.getParentFile() != null) {
      File srcDir = new File(libOrPluginsFile.getParentFile(), "src");

      int endIndex = libFile.getName().indexOf(gradleVersion);
      if (endIndex != -1) {
        String srcDirChild = libFile.getName().substring("gradle-".length(), endIndex - 1);
        srcDir = new File(srcDir, srcDirChild);
      }

      if (srcDir.isDirectory()) {
        library.addPath(LibraryPathType.SOURCE, srcDir.getPath());
      }
    }
  }

  public static void attachSourcesAndJavadocFromGradleCacheIfNeeded(File gradleUserHomeDir, LibraryData libraryData) {
    attachSourcesAndJavadocFromGradleCacheIfNeeded(null, gradleUserHomeDir, libraryData);
  }

  private static final Key<Set<LibraryData>> LIBRARIES_CACHE =
    Key.create("GradleProjectResolverUtil.LIBRARIES_CACHE");
  private static final Key<Map<String, Map<LibraryPathType, List<String>>>> PATHS_CACHE =
    Key.create("GradleProjectResolverUtil.PATHS_CACHE");

  public static void attachSourcesAndJavadocFromGradleCacheIfNeeded(ProjectResolverContext context,
                                                                    File gradleUserHomeDir,
                                                                    LibraryData libraryData) {
    boolean sourceResolved = !libraryData.getPaths(LibraryPathType.SOURCE).isEmpty();
    boolean docResolved = !libraryData.getPaths(LibraryPathType.DOC).isEmpty();
    if (sourceResolved && docResolved) {
      return;
    }

    Map<String, Map<LibraryPathType, List<String>>> pathsCache = null;
    if (context != null) {
      // skip already processed libraries
      Set<LibraryData> libsCache = ConcurrencyUtil.computeIfAbsent(context, LIBRARIES_CACHE, () -> Collections.newSetFromMap(new IdentityHashMap<>()));
      if (!libsCache.add(libraryData)) return;

      pathsCache = ConcurrencyUtil.computeIfAbsent(context, PATHS_CACHE, () -> new HashMap<>());
    }

    for (String path : libraryData.getPaths(LibraryPathType.BINARY)) {
      if (!FileUtil.isAncestor(gradleUserHomeDir.getPath(), path, true)) continue;

      // take already processed paths from cache
      Map<LibraryPathType, List<String>> collectedPaths = pathsCache == null ? null : pathsCache.get(path);
      if (collectedPaths == null) {
        collectedPaths = new HashMap<>();
        if (pathsCache != null) {
          pathsCache.put(path, collectedPaths);
        }
        if (path.contains("/caches/modules-2/files-2.1") || path.contains("/caches/transforms-")) {
          UnifiedCoordinates coordinates = getLibraryCoordinates(libraryData);
          Set<LibraryPathType> requiredComponentTypes = getRequiredComponentTypes(sourceResolved, docResolved);
          final Map<LibraryPathType, List<Path>> cachedComponents;
          if (coordinates != null) {
            cachedComponents = findArtifactComponents(coordinates, gradleUserHomeDir.toPath(), requiredComponentTypes);
          }
          else {
            cachedComponents = findAdjacentComponents(Paths.get(path), requiredComponentTypes);
          }
          mergeCollectedArtifacts(collectedPaths, cachedComponents);
        }
        else {
          collectSourcesAndJavadocsFromTheSameFolder(path, collectedPaths, sourceResolved, docResolved);
        }
      }

      for (Map.Entry<LibraryPathType, List<String>> each : collectedPaths.entrySet()) {
        for (String cachedPath : each.getValue()) {
          libraryData.addPath(each.getKey(), cachedPath);
        }
      }
    }
  }

  private static void mergeCollectedArtifacts(@NotNull Map<LibraryPathType, List<String>> target,
                                              @NotNull Map<LibraryPathType, List<Path>> collected) {
    for (Map.Entry<LibraryPathType, List<Path>> entry : collected.entrySet()) {
      Set<String> pathStrings = entry.getValue()
        .stream()
        .map(v -> v.toString())
        .collect(Collectors.toSet());
      target.computeIfAbsent(entry.getKey(), ignore -> new SmartList<>()).addAll(pathStrings);
    }
  }

  private static @NotNull Set<LibraryPathType> getRequiredComponentTypes(boolean sourceResolved, boolean docResolved) {
    Set<LibraryPathType> requiredComponentTypes = EnumSet.noneOf(LibraryPathType.class);
    if (!sourceResolved) {
      requiredComponentTypes.add(LibraryPathType.SOURCE);
    }
    if (!docResolved) {
      requiredComponentTypes.add(LibraryPathType.DOC);
    }
    return requiredComponentTypes;
  }

  private static void collectSourcesAndJavadocsFromTheSameFolder(@NotNull String binaryPath,
                                                                 @NotNull Map<LibraryPathType, List<String>> collect,
                                                                 boolean sourceResolved,
                                                                 boolean docResolved) {
    final Path file = Paths.get(binaryPath);
    Path binaryFileParent = file.getParent();
    if (!Files.isDirectory(binaryFileParent)) return;
    try (Stream<Path> list = Files.list(binaryFileParent)) {
      for (Iterator<Path> it = list.iterator(); it.hasNext(); ) {
        Path p = it.next();
        if (!Files.isRegularFile(p)) continue;

        String name = p.getFileName().toString();
        if (!sourceResolved && name.endsWith(SOURCE_JAR_SUFFIX)) {
          collect.computeIfAbsent(LibraryPathType.SOURCE, type -> new SmartList<>()).add(p.toFile().getPath());
          sourceResolved = true;
        }
        else if (!docResolved && name.endsWith(JAVADOC_JAR_SUFFIX)) {
          collect.computeIfAbsent(LibraryPathType.DOC, type -> new SmartList<>()).add(p.toFile().getPath());
          docResolved = true;
        }
        if (sourceResolved && docResolved) {
          return;
        }
      }
    }
    catch (IOException e) {
      LOG.debug(e);
    }
  }

  private static @Nullable UnifiedCoordinates getLibraryCoordinates(@NotNull LibraryData libraryData) {
    String group = libraryData.getGroupId();
    String artifact = libraryData.getArtifactId();
    String version = libraryData.getVersion();
    if (hasNull(group, artifact, version)) {
      String externalName = libraryData.getExternalName();
      if (StringUtil.isEmpty(externalName)) {
        return null;
      }
      String[] externalNameParticles = externalName.split(":");
      if (externalNameParticles.length == 3) {
        group = externalNameParticles[0];
        artifact = externalNameParticles[1];
        version = externalNameParticles[2];
      }
    }
    if (hasNull(group, artifact, version)) {
      return null;
    }
    version = version.replace("@aar", "");
    return new UnifiedCoordinates(group, artifact, version);
  }

  public static void buildDependencies(@NotNull ProjectResolverContext resolverCtx,
                                       @NotNull Map<String, Pair<DataNode<GradleSourceSetData>, ExternalSourceSet>> sourceSetMap,
                                       final @NotNull ArtifactMappingService artifactsMap,
                                       @NotNull DataNode<? extends ExternalEntityData> ownerDataNode,
                                       @NotNull Collection<ExternalDependency> dependencies,
                                       @Nullable DataNode<ProjectData> ideProject) throws IllegalStateException {
    Map<ExternalDependencyId, ExternalDependency> dependencyMap = new HashMap<>();

    Queue<ExternalDependency> queue = new ArrayDeque<>(dependencies);
    while (!queue.isEmpty()) {
      final ExternalDependency dependency = queue.remove();
      DefaultExternalDependencyId key = new DefaultExternalDependencyId(dependency.getId());
      ExternalDependency seenDependency = dependencyMap.get(key);
      if (seenDependency != null) {
        if (dependency instanceof ExternalLibraryDependency libDependency) {
          if (seenDependency instanceof ExternalLibraryDependency seenLibDependency &&
              !FileUtil.filesEqual(seenLibDependency.getFile(), libDependency.getFile())) {
            DefaultExternalMultiLibraryDependency mergedDependency = new DefaultExternalMultiLibraryDependency(seenLibDependency);
            mergedDependency.setScope(getMergedDependencyScope(mergedDependency.getScope(), libDependency.getScope()).name());
            mergedDependency.addArtifactsFrom(libDependency);
            dependencyMap.put(key, mergedDependency);
            continue;
          }
          else if (seenDependency instanceof DefaultExternalMultiLibraryDependency mergedDependency) {
            mergedDependency.setScope(getMergedDependencyScope(mergedDependency.getScope(), libDependency.getScope()).name());
            mergedDependency.addArtifactsFrom(libDependency);
            continue;
          }
        }

        DependencyScope prevScope = getDependencyScope(seenDependency.getScope());
        DependencyScope currentScope = getDependencyScope(dependency.getScope());

        if (prevScope.isForProductionCompile()) continue;
        if (prevScope.isForProductionRuntime() && currentScope.isForProductionRuntime()) continue;
      }

      dependencyMap.put(key, dependency);
