// 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.gradle.toolingExtension.impl.model.buildScriptClasspathModel.GradleBuildScriptClasspathModelProvider;
import com.intellij.gradle.toolingExtension.impl.model.projectModel.GradleExternalProjectModelProvider;
import com.intellij.gradle.toolingExtension.impl.model.sourceSetDependencyModel.GradleSourceSetDependencyModelProvider;
import com.intellij.gradle.toolingExtension.impl.model.sourceSetModel.GradleSourceSetModelProvider;
import com.intellij.gradle.toolingExtension.impl.model.taskModel.GradleTaskModelProvider;
import com.intellij.gradle.toolingExtension.util.GradleVersionUtil;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.model.ConfigurationDataImpl;
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.model.task.ExternalSystemTaskId;
import com.intellij.openapi.externalSystem.model.task.TaskData;
import com.intellij.openapi.externalSystem.service.notification.ExternalSystemNotificationManager;
import com.intellij.openapi.externalSystem.service.notification.NotificationCategory;
import com.intellij.openapi.externalSystem.service.notification.NotificationData;
import com.intellij.openapi.externalSystem.service.notification.NotificationSource;
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
import com.intellij.openapi.externalSystem.util.Order;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.DependencyScope;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.CanonicalPathPrefixTree;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.FileCollectionFactory;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.containers.prefixTree.map.MutablePrefixTreeMap;
import com.intellij.util.execution.ParametersListUtil;
import org.gradle.tooling.model.DomainObjectSet;
import org.gradle.tooling.model.GradleModuleVersion;
import org.gradle.tooling.model.GradleTask;
import org.gradle.tooling.model.UnsupportedMethodException;
import org.gradle.tooling.model.idea.*;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.issue.UnresolvedDependencySyncIssue;
import org.jetbrains.plugins.gradle.model.*;
import org.jetbrains.plugins.gradle.model.data.BuildScriptClasspathData;
import org.jetbrains.plugins.gradle.model.data.GradleProjectBuildScriptData;
import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData;
import org.jetbrains.plugins.gradle.model.tests.ExternalTestSourceMapping;
import org.jetbrains.plugins.gradle.model.tests.ExternalTestsModel;
import org.jetbrains.plugins.gradle.service.project.data.ExternalProjectDataCache;
import org.jetbrains.plugins.gradle.service.project.data.GradleExtensionsDataService;
import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings;
import org.jetbrains.plugins.gradle.util.GradleBundle;
import org.jetbrains.plugins.gradle.util.GradleConstants;
import org.jetbrains.plugins.gradle.util.GradleModuleData;
import org.jetbrains.plugins.gradle.util.GradleUtil;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static com.intellij.openapi.util.text.StringUtil.*;
import static org.jetbrains.plugins.gradle.service.project.GradleProjectResolver.MODULES_OUTPUTS;
import static org.jetbrains.plugins.gradle.service.project.GradleProjectResolverUtil.*;

/**
 * {@link CommonGradleProjectResolverExtension} provides implementation of Gradle project resolver common to all project types.
 *
 * @author Vladislav.Soroka
 */
@Order(Integer.MAX_VALUE - 1)
public final class CommonGradleProjectResolverExtension extends AbstractProjectResolverExtension {
  private static final Logger LOG = Logger.getInstance(CommonGradleProjectResolverExtension.class);

  private static final @NotNull @NonNls String UNRESOLVED_DEPENDENCY_PREFIX = "unresolved dependency - ";

  public static final String GRADLE_VERSION_CATALOGS_DYNAMIC_SUPPORT = "gradle.version.catalogs.dynamic.support";

  @Override
  public void populateProjectExtraModels(@NotNull IdeaProject gradleProject, @NotNull DataNode<ProjectData> ideProject) {
    final ExternalProject externalProject = resolverCtx.getRootModel(ExternalProject.class);
    if (externalProject != null) {
      ideProject.createChild(ExternalProjectDataCache.KEY, externalProject);
      ideProject.getData().setDescription(externalProject.getDescription());
    }

    final IntelliJSettings intellijSettings = resolverCtx.getRootModel(IntelliJProjectSettings.class);
    if (intellijSettings != null) {
      ideProject.createChild(ProjectKeys.CONFIGURATION,
                             new ConfigurationDataImpl(GradleConstants.SYSTEM_ID, intellijSettings.getSettings()));
    }

    final DependencyAccessorsModel dependencyAccessorsModel = resolverCtx.getRootModel(DependencyAccessorsModel.class);
    if (dependencyAccessorsModel != null && Registry.is(GRADLE_VERSION_CATALOGS_DYNAMIC_SUPPORT, false)) {
      ideProject.createChild(BuildScriptClasspathData.ACCESSORS, dependencyAccessorsModel);
    }

    final VersionCatalogsModel versionCatalogsModel = resolverCtx.getRootModel(VersionCatalogsModel.class);
    if (versionCatalogsModel != null) {
      ideProject.createChild(BuildScriptClasspathData.VERSION_CATALOGS, versionCatalogsModel);
    }
  }

  @Override
  @SuppressWarnings("deprecation")
  public @NotNull DataNode<ModuleData> createModule(@NotNull IdeaModule gradleModule, @NotNull DataNode<ProjectData> projectDataNode) {
    DataNode<ModuleData> mainModuleNode = createMainModule(resolverCtx, gradleModule, projectDataNode);
    final ModuleData mainModuleData = mainModuleNode.getData();
    final String mainModuleConfigPath = mainModuleData.getLinkedExternalProjectPath();
    final String mainModuleFileDirectoryPath = mainModuleData.getModuleFileDirectoryPath();

    String[] moduleGroup = null;
    if (!resolverCtx.isUseQualifiedModuleNames()) {
      moduleGroup = getIdeModuleGroup(mainModuleData.getInternalName(), gradleModule);
      mainModuleData.setIdeModuleGroup(moduleGroup);
    }

    if (resolverCtx.isResolveModulePerSourceSet()) {
      ExternalProject externalProject = getExternalProject(gradleModule, resolverCtx);
      assert externalProject != null;
      for (ExternalSourceSet sourceSet : externalProject.getSourceSets().values()) {
        final String moduleId = getModuleId(resolverCtx, gradleModule, sourceSet);
        final String moduleExternalName = gradleModule.getName() + ":" + sourceSet.getName();
        final String moduleInternalName = getInternalModuleName(gradleModule, externalProject, sourceSet.getName(), resolverCtx);

        GradleSourceSetData sourceSetData = new GradleSourceSetData(
          moduleId, moduleExternalName, moduleInternalName, mainModuleFileDirectoryPath, mainModuleConfigPath);

        sourceSetData.setGroup(externalProject.getGroup());
        if ("main".equals(sourceSet.getName())) {
          sourceSetData.setPublication(new ProjectId(externalProject.getGroup(),
                                                     externalProject.getName(),
                                                     externalProject.getVersion()));
        }
        sourceSetData.setVersion(externalProject.getVersion());
        sourceSetData.setIdeModuleGroup(moduleGroup);

        final Set<File> artifacts = FileCollectionFactory.createCanonicalFileSet();
        if ("main".equals(sourceSet.getName())) {
          final Set<File> defaultArtifacts = externalProject.getArtifactsByConfiguration().get("default");
          if (defaultArtifacts != null) {
            artifacts.addAll(defaultArtifacts);
          }
        }
        else {
          if ("test".equals(sourceSet.getName())) {
            sourceSetData.setProductionModuleId(getInternalModuleName(gradleModule, externalProject, "main", resolverCtx));
            final Set<File> testsArtifacts = externalProject.getArtifactsByConfiguration().get("tests");
            if (testsArtifacts != null) {
              artifacts.addAll(testsArtifacts);
            }
          }
        }
        artifacts.addAll(sourceSet.getArtifacts());
        for (ExternalSourceDirectorySet directorySet : sourceSet.getSources().values()) {
          artifacts.addAll(directorySet.getGradleOutputDirs());
        }
        sourceSetData.setArtifacts(new ArrayList<>(artifacts));

        DataNode<GradleSourceSetData> sourceSetDataNode = mainModuleNode.createChild(GradleSourceSetData.KEY, sourceSetData);
        final Map<String, Pair<DataNode<GradleSourceSetData>, ExternalSourceSet>> sourceSetMap =
          projectDataNode.getUserData(GradleProjectResolver.RESOLVED_SOURCE_SETS);
        assert sourceSetMap != null;
        sourceSetMap.put(moduleId, Pair.create(sourceSetDataNode, sourceSet));
      }
    }

    final ProjectData projectData = projectDataNode.getData();
    if (StringUtil.equals(mainModuleData.getLinkedExternalProjectPath(), projectData.getLinkedExternalProjectPath())) {
      projectData.setGroup(mainModuleData.getGroup());
      projectData.setVersion(mainModuleData.getVersion());
    }
    populateBuildScriptSource(gradleModule, mainModuleNode);

    return mainModuleNode;
  }

  private static void populateBuildScriptSource(@NotNull IdeaModule ideaModule, @NotNull DataNode<? extends ModuleData> mainModuleNode) {
    try {
      File buildScriptSource = ideaModule.getGradleProject().getBuildScript().getSourceFile();
      GradleProjectBuildScriptData buildProjectData = new GradleProjectBuildScriptData(buildScriptSource);
      mainModuleNode.createChild(GradleProjectBuildScriptData.KEY, buildProjectData);
    }
    catch (UnsupportedMethodException ignore) {
    }
  }

  private static String @NotNull [] getIdeModuleGroup(String moduleName, IdeaModule gradleModule) {
    final String gradlePath = gradleModule.getGradleProject().getPath();
    final String rootName = gradleModule.getProject().getName();
    if (isEmpty(gradlePath) || ":".equals(gradlePath)) {
      return new String[]{moduleName};
    }
    else {
      return (rootName + gradlePath).split(":");
    }
  }

  @Override
  public void populateModuleExtraModels(@NotNull IdeaModule gradleModule, @NotNull DataNode<ModuleData> ideModule) {
    GradleExtensions gradleExtensions = resolverCtx.getExtraProject(gradleModule, GradleExtensions.class);
    if (gradleExtensions != null) {
      //noinspection ConditionCoveredByFurtherCondition
      boolean isGradleProxyClass = gradleExtensions instanceof Proxy || !(gradleExtensions instanceof DefaultGradleExtensions);
      DefaultGradleExtensions extensions = isGradleProxyClass ? new DefaultGradleExtensions(gradleExtensions)
                                                              : (DefaultGradleExtensions)gradleExtensions;
      ExternalProject externalProject = getExternalProject(gradleModule, resolverCtx);
      if (externalProject != null) {
        extensions.addTasks(externalProject.getTasks().values());
      }
      ideModule.createChild(GradleExtensionsDataService.KEY, extensions);
    }

    final IntelliJSettings intellijSettings = resolverCtx.getExtraProject(gradleModule, IntelliJSettings.class);
    if (intellijSettings != null) {
      ideModule.createChild(ProjectKeys.CONFIGURATION,
                            new ConfigurationDataImpl(GradleConstants.SYSTEM_ID, intellijSettings.getSettings()));
    }

    ExternalTestsModel externalTestsModel = resolverCtx.getProjectModel(gradleModule, ExternalTestsModel.class);
    if (externalTestsModel != null) {
      for (ExternalTestSourceMapping testSourceMapping : externalTestsModel.getTestSourceMappings()) {
        String testName = testSourceMapping.getTestName();
        String testTaskName = testSourceMapping.getTestTaskPath();
        Set<String> sourceFolders = testSourceMapping.getSourceFolders();
        TestData testData = new TestData(GradleConstants.SYSTEM_ID, testName, testTaskName, sourceFolders);
        ideModule.createChild(ProjectKeys.TEST, testData);
      }
    }
  }

  @Override
  public void populateModuleContentRoots(@NotNull IdeaModule gradleModule,
                                         @NotNull DataNode<ModuleData> ideModule) {
    ExternalProject externalProject = getExternalProject(gradleModule, resolverCtx);
    Set<String> sourceSetContentRoots = Set.of();
    if (externalProject != null) {
      sourceSetContentRoots = addExternalProjectContentRoots(gradleModule, ideModule, externalProject);
    }

    MutablePrefixTreeMap<String, ContentRootData> contentRootIndex = CanonicalPathPrefixTree.INSTANCE.createMap();
    for (DataNode<ContentRootData> contentRootDataNode : ExternalSystemApiUtil.findAll(ideModule, ProjectKeys.CONTENT_ROOT)) {
      ContentRootData contentRootData = contentRootDataNode.getData();
      contentRootIndex.put(contentRootData.getRootPath(), contentRootData);
    }

    DomainObjectSet<? extends IdeaContentRoot> contentRoots = gradleModule.getContentRoots();
    if (contentRoots == null) return;
    for (IdeaContentRoot gradleContentRoot : contentRoots) {
      if (gradleContentRoot == null) continue;

      File rootDirectory = gradleContentRoot.getRootDirectory();
      if (rootDirectory == null) continue;

      boolean oldGradle = false;


      String contentRootPath = FileUtil.toCanonicalPath(rootDirectory.getPath());

      // don't add content root if one of source sets already have this content root
      boolean sameAsSourceSetContentRoot = sourceSetContentRoots.contains(contentRootPath);

      if (!sameAsSourceSetContentRoot) {
        ContentRootData ideContentRoot = new ContentRootData(GradleConstants.SYSTEM_ID, contentRootPath);
        contentRootIndex.put(contentRootPath, ideContentRoot);

        Set<File> excluded = gradleContentRoot.getExcludeDirectories();
        if (excluded != null) {
          for (File file : excluded) {
            ideContentRoot.storePath(ExternalSystemSourceType.EXCLUDED, file.getPath());
          }
        }
      }

      boolean sourceSetsAreNotConfigured = sourceSetContentRoots.isEmpty();

      if (!resolverCtx.isResolveModulePerSourceSet() || sourceSetsAreNotConfigured) {
        List<? extends IdeaSourceDirectory> sourceDirectories = gradleContentRoot.getSourceDirectories().getAll();
        List<? extends IdeaSourceDirectory> testDirectories = gradleContentRoot.getTestDirectories().getAll();
        List<? extends IdeaSourceDirectory> resourceDirectories = Collections.emptyList();
        List<? extends IdeaSourceDirectory> testResourceDirectories = Collections.emptyList();
        try {
          final Set<File> notResourceDirs = collectExplicitNonResourceDirectories(externalProject);

          resourceDirectories = gradleContentRoot.getResourceDirectories().getAll();
          removeDuplicateResources(sourceDirectories, resourceDirectories, notResourceDirs);
          testResourceDirectories = gradleContentRoot.getTestResourceDirectories().getAll();
          removeDuplicateResources(testDirectories, testResourceDirectories, notResourceDirs);
        }
        catch (UnsupportedMethodException e) {
          oldGradle = true;
          // org.gradle.tooling.model.idea.IdeaContentRoot.getResourceDirectories/getTestResourceDirectories methods supported since Gradle 4.7
          LOG.debug(e.getMessage());

          if (externalProject == null) {
            populateContentRoot(contentRootIndex, ExternalSystemSourceType.SOURCE, gradleContentRoot.getSourceDirectories());
            populateContentRoot(contentRootIndex, ExternalSystemSourceType.TEST, gradleContentRoot.getTestDirectories());
          }
        }

        if (!oldGradle) {
          populateContentRoot(contentRootIndex, ExternalSystemSourceType.SOURCE, sourceDirectories);
          populateContentRoot(contentRootIndex, ExternalSystemSourceType.TEST, testDirectories);
          populateContentRoot(contentRootIndex, ExternalSystemSourceType.RESOURCE, resourceDirectories);
          populateContentRoot(contentRootIndex, ExternalSystemSourceType.TEST_RESOURCE, testResourceDirectories);
        }
      }
    }
    Set<String> existsContentRoots = new LinkedHashSet<>();
    for (DataNode<ContentRootData> contentRootDataNode : ExternalSystemApiUtil.findAll(ideModule, ProjectKeys.CONTENT_ROOT)) {
      ContentRootData contentRootData = contentRootDataNode.getData();
      existsContentRoots.add(contentRootData.getRootPath());
    }
    for (ContentRootData ideContentRoot : contentRootIndex.values()) {
      if (!existsContentRoots.contains(ideContentRoot.getRootPath())) {
        ideModule.createChild(ProjectKeys.CONTENT_ROOT, ideContentRoot);
      }
    }
  }

  private static @Nullable ExternalProject getExternalProject(@NotNull IdeaModule gradleModule, @NotNull ProjectResolverContext resolverCtx) {
    ExternalProject project = resolverCtx.getExtraProject(gradleModule, ExternalProject.class);
    if (project == null && resolverCtx.isResolveModulePerSourceSet()) {
      LOG.error("External Project model is missing for module-per-sourceSet import mode. Please, check import log for error messages.");
    }
    return project;
  }

  private Set<String> addExternalProjectContentRoots(@NotNull IdeaModule gradleModule,
                                                     @NotNull DataNode<ModuleData> ideModule,
                                                     @NotNull ExternalProject externalProject) {
    Set<String> contentsRoots = new LinkedHashSet<>();
    final String buildDirPath = FileUtil.toCanonicalPath(externalProject.getBuildDir().getPath());

    processSourceSets(resolverCtx, gradleModule, externalProject, ideModule, new SourceSetsProcessor() {
      @Override
      public void process(@NotNull DataNode<? extends ModuleData> dataNode, @NotNull ExternalSourceSet sourceSet) {
        sourceSet.getSources().forEach((key, sourceDirectorySet) -> {
          ExternalSystemSourceType sourceType = ExternalSystemSourceType.from(key);
          for (File file : sourceDirectorySet.getSrcDirs()) {
            String path = FileUtil.toCanonicalPath(file.getPath());
            contentsRoots.add(path);
            ContentRootData ideContentRoot = new ContentRootData(GradleConstants.SYSTEM_ID, path);
            if (FileUtil.isAncestor(path, buildDirPath, true)) {
              ideContentRoot.storePath(ExternalSystemSourceType.EXCLUDED, buildDirPath);
            }
            ideContentRoot.storePath(sourceType, path);
            dataNode.createChild(ProjectKeys.CONTENT_ROOT, ideContentRoot);
          }
        });
      }
    });

    return contentsRoots;
  }

  private static void removeDuplicateResources(@NotNull List<? extends IdeaSourceDirectory> sourceDirectories,
                                               @NotNull List<? extends IdeaSourceDirectory> resourceDirectories,
                                               @NotNull Set<File> notResourceDirs) {


    resourceDirectories.removeIf(ideaSourceDirectory -> notResourceDirs.contains(ideaSourceDirectory.getDirectory()));
    removeAll(sourceDirectories, resourceDirectories);
  }

  private static @NotNull Set<File> collectExplicitNonResourceDirectories(@Nullable ExternalProject externalProject) {
    if (externalProject == null) {
      return Collections.emptySet();
    }

    return externalProject.getSourceSets().values().stream()
      .flatMap(ss -> ss.getSources().entrySet().stream()
        .filter(e -> !e.getKey().isResource())
        .flatMap(e -> e.getValue().getSrcDirs().stream()))
      .collect(Collectors.toCollection(() -> FileCollectionFactory.createCanonicalFileSet()));
  }

  private static void removeAll(List<? extends IdeaSourceDirectory> list, List<? extends IdeaSourceDirectory> toRemove) {
    Set<File> files = toRemove.stream().map(o -> o.getDirectory()).collect(Collectors.toSet());
    list.removeIf(o -> files.contains(o.getDirectory()));
  }

  private static void processSourceSets(@NotNull ProjectResolverContext resolverCtx,
                                        @NotNull IdeaModule gradleModule,
                                        @NotNull ExternalProject externalProject,
                                        @NotNull DataNode<ModuleData> ideModule,
                                        @NotNull SourceSetsProcessor processor) {
    Map<String, DataNode<GradleSourceSetData>> sourceSetsMap = new HashMap<>();
    for (DataNode<GradleSourceSetData> dataNode : ExternalSystemApiUtil.findAll(ideModule, GradleSourceSetData.KEY)) {
      sourceSetsMap.put(dataNode.getData().getId(), dataNode);
    }

    for (ExternalSourceSet sourceSet : externalProject.getSourceSets().values()) {
      if (sourceSet == null || sourceSet.getSources().isEmpty()) continue;

      final String moduleId = getModuleId(resolverCtx, gradleModule, sourceSet);
      final DataNode<? extends ModuleData> moduleDataNode = sourceSetsMap.isEmpty() ? ideModule : sourceSetsMap.get(moduleId);
      if (moduleDataNode == null) continue;

      processor.process(moduleDataNode, sourceSet);
    }
  }


  @Override
  public void populateModuleCompileOutputSettings(@NotNull IdeaModule gradleModule,
                                                  @NotNull DataNode<ModuleData> ideModule) {
    ModuleData moduleData = ideModule.getData();
    moduleData.useExternalCompilerOutput(resolverCtx.isDelegatedBuild());

    File ideaOutDir = new File(moduleData.getLinkedExternalProjectPath(), "out");

    ExternalProject externalProject = getExternalProject(gradleModule, resolverCtx);
    if (resolverCtx.isResolveModulePerSourceSet()) {
      DataNode<ProjectData> projectDataNode = ideModule.getDataNode(ProjectKeys.PROJECT);
      assert projectDataNode != null;
      final Map<String, Pair<String, ExternalSystemSourceType>> moduleOutputsMap = projectDataNode.getUserData(MODULES_OUTPUTS);
      assert moduleOutputsMap != null;

      Set<String> outputDirs = new HashSet<>();
      assert externalProject != null;
      processSourceSets(resolverCtx, gradleModule, externalProject, ideModule, new SourceSetsProcessor() {
        @Override
        public void process(@NotNull DataNode<? extends ModuleData> dataNode, @NotNull ExternalSourceSet sourceSet) {
          MultiMap<ExternalSystemSourceType, String> gradleOutputMap = dataNode.getUserData(GradleProjectResolver.GRADLE_OUTPUTS);
          if (gradleOutputMap == null) {
            gradleOutputMap = MultiMap.create();
            dataNode.putUserData(GradleProjectResolver.GRADLE_OUTPUTS, gradleOutputMap);
          }
          final ModuleData moduleData = dataNode.getData();
          moduleData.useExternalCompilerOutput(resolverCtx.isDelegatedBuild());
          for (Map.Entry<? extends IExternalSystemSourceType, ? extends ExternalSourceDirectorySet> directorySetEntry
            : sourceSet.getSources().entrySet()) {
            ExternalSystemSourceType sourceType = ExternalSystemSourceType.from(directorySetEntry.getKey());
            ExternalSourceDirectorySet sourceDirectorySet = directorySetEntry.getValue();
            File ideOutputDir = getIdeOutputDir(sourceDirectorySet);
            File gradleOutputDir = getGradleOutputDir(sourceDirectorySet);
            File outputDir = resolverCtx.isDelegatedBuild() ? gradleOutputDir : ideOutputDir;
            moduleData.setCompileOutputPath(sourceType, ideOutputDir == null ? null : ideOutputDir.getPath());
            moduleData.setExternalCompilerOutputPath(sourceType, gradleOutputDir == null ? null : gradleOutputDir.getPath());
            moduleData.setInheritProjectCompileOutputPath(sourceDirectorySet.isCompilerOutputPathInherited());

            if (outputDir != null) {
              outputDirs.add(outputDir.getPath());
              for (File file : sourceDirectorySet.getGradleOutputDirs()) {
                String gradleOutputPath = ExternalSystemApiUtil.toCanonicalPath(file.getPath());
                gradleOutputMap.putValue(sourceType, gradleOutputPath);
                if (!file.getPath().equals(outputDir.getPath())) {
                  moduleOutputsMap.put(gradleOutputPath, Pair.create(moduleData.getId(), sourceType));
                }
              }
            }
          }
        }
      });
      if (outputDirs.stream().anyMatch(path -> FileUtil.isAncestor(ideaOutDir, new File(path), false))) {
        GradleUtil.excludeOutDir(ideModule, ideaOutDir);
      }
      return;
    }

    IdeaCompilerOutput moduleCompilerOutput = gradleModule.getCompilerOutput();
    boolean inheritOutputDirs = moduleCompilerOutput != null && moduleCompilerOutput.getInheritOutputDirs();

    if (moduleCompilerOutput != null) {
      File outputDir = moduleCompilerOutput.getOutputDir();
      if (outputDir != null) {
        moduleData.setCompileOutputPath(ExternalSystemSourceType.SOURCE, outputDir.getPath());
        moduleData.setCompileOutputPath(ExternalSystemSourceType.RESOURCE, outputDir.getPath());
        moduleData.setExternalCompilerOutputPath(ExternalSystemSourceType.SOURCE, outputDir.getPath());
        moduleData.setExternalCompilerOutputPath(ExternalSystemSourceType.RESOURCE, outputDir.getPath());
      }
      else {
        moduleData.setCompileOutputPath(ExternalSystemSourceType.SOURCE, new File(ideaOutDir, "production/classes").getPath());
        moduleData.setCompileOutputPath(ExternalSystemSourceType.RESOURCE, new File(ideaOutDir, "production/resources").getPath());
        if (externalProject != null) {
          File gradleOutputDir = getGradleOutputDir(externalProject, "main", ExternalSystemSourceType.SOURCE);
          moduleData.setExternalCompilerOutputPath(ExternalSystemSourceType.SOURCE,
                                                   gradleOutputDir == null ? null : gradleOutputDir.getPath());
          File gradleResourceOutputDir = getGradleOutputDir(externalProject, "main", ExternalSystemSourceType.RESOURCE);
          moduleData.setExternalCompilerOutputPath(ExternalSystemSourceType.RESOURCE,
                                                   gradleResourceOutputDir == null ? null : gradleResourceOutputDir.getPath());
        }
      }

      File testOutputDir = moduleCompilerOutput.getTestOutputDir();
      if (testOutputDir != null) {
        moduleData.setCompileOutputPath(ExternalSystemSourceType.TEST, testOutputDir.getPath());
        moduleData.setCompileOutputPath(ExternalSystemSourceType.TEST_RESOURCE, testOutputDir.getPath());
        moduleData.setExternalCompilerOutputPath(ExternalSystemSourceType.TEST, testOutputDir.getPath());
        moduleData.setExternalCompilerOutputPath(ExternalSystemSourceType.TEST_RESOURCE, testOutputDir.getPath());
      }
      else {
        moduleData.setCompileOutputPath(ExternalSystemSourceType.TEST, new File(ideaOutDir, "test/classes").getPath());
        moduleData.setCompileOutputPath(ExternalSystemSourceType.TEST_RESOURCE, new File(ideaOutDir, "test/resources").getPath());
        if (externalProject != null) {
          File gradleOutputDir = getGradleOutputDir(externalProject, "test", ExternalSystemSourceType.TEST);
          moduleData.setExternalCompilerOutputPath(ExternalSystemSourceType.TEST,
                                                   gradleOutputDir == null ? null : gradleOutputDir.getPath());
          File gradleResourceOutputDir = getGradleOutputDir(externalProject, "test", ExternalSystemSourceType.TEST_RESOURCE);
          moduleData.setExternalCompilerOutputPath(ExternalSystemSourceType.TEST_RESOURCE,
                                                   gradleResourceOutputDir == null ? null : gradleResourceOutputDir.getPath());
        }
      }

      if (!resolverCtx.isDelegatedBuild() && !inheritOutputDirs && (outputDir == null || testOutputDir == null)) {
        GradleUtil.excludeOutDir(ideModule, ideaOutDir);
      }
    }

    moduleData.setInheritProjectCompileOutputPath(inheritOutputDirs);
  }

  public static @Nullable File getGradleOutputDir(@NotNull ExternalProject externalProject,
                                                  @NotNull String sourceSetName,
                                                  @NotNull ExternalSystemSourceType sourceType) {
    ExternalSourceSet sourceSet = externalProject.getSourceSets().get(sourceSetName);
    if (sourceSet == null) return null;
    return getGradleOutputDir(sourceSet.getSources().get(sourceType));
  }

  private static @Nullable File getIdeOutputDir(@Nullable ExternalSourceDirectorySet sourceDirectorySet) {
    if (sourceDirectorySet == null) return null;
    return sourceDirectorySet.getOutputDir();
  }

  private static @Nullable File getGradleOutputDir(@Nullable ExternalSourceDirectorySet sourceDirectorySet) {
    if (sourceDirectorySet == null) return null;
    Set<File> srcDirs = sourceDirectorySet.getSrcDirs();
    Collection<File> outputDirectories = sourceDirectorySet.getGradleOutputDirs();
    String firstExistingLang = srcDirs.stream()
      .sorted()
      .filter(File::exists)
      .findFirst()
      .map(File::getName)
      .orElse(null);

    if (firstExistingLang == null) {
      return ContainerUtil.getFirstItem(outputDirectories);
    }

    return outputDirectories.stream()
      .filter(f -> f.getPath().contains(firstExistingLang))
      .findFirst()
      .orElse(ContainerUtil.getFirstItem(outputDirectories));
  }

  @Override
  public void populateModuleDependencies(@NotNull IdeaModule gradleModule,
                                         @NotNull DataNode<ModuleData> ideModule,
                                         final @NotNull DataNode<ProjectData> ideProject) {

    ExternalProject externalProject = getExternalProject(gradleModule, resolverCtx);
    if (resolverCtx.isResolveModulePerSourceSet()) {
      final Map<String, Pair<DataNode<GradleSourceSetData>, ExternalSourceSet>> sourceSetMap =
        ideProject.getUserData(GradleProjectResolver.RESOLVED_SOURCE_SETS);
      final ArtifactMappingService artifactsMap = resolverCtx.getArtifactsMap();
      assert sourceSetMap != null;
      assert artifactsMap != null;
      assert externalProject != null;
      processSourceSets(resolverCtx, gradleModule, externalProject, ideModule, new SourceSetsProcessor() {
        @Override
        public void process(@NotNull DataNode<? extends ModuleData> dataNode, @NotNull ExternalSourceSet sourceSet) {
          buildDependencies(resolverCtx, sourceSetMap, artifactsMap, dataNode, sourceSet.getDependencies(), ideProject);
        }
      });
      return;
    }

    final List<? extends IdeaDependency> dependencies = gradleModule.getDependencies().getAll();

    if (dependencies == null) return;

    List<String> orphanModules = new ArrayList<>();
    Map<String, ModuleData> modulesIndex = new HashMap<>();

    for (DataNode<ModuleData> dataNode : ExternalSystemApiUtil.getChildren(ideProject, ProjectKeys.MODULE)) {
      modulesIndex.put(dataNode.getData().getExternalName(), dataNode.getData());
    }

    for (int i = 0; i < dependencies.size(); i++) {
      IdeaDependency dependency = dependencies.get(i);
      if (dependency == null) {
        continue;
      }
      DependencyScope scope = parseScope(dependency.getScope());

      if (dependency instanceof IdeaModuleDependency) {
        ModuleDependencyData d = buildDependency(resolverCtx, ideModule, (IdeaModuleDependency)dependency, modulesIndex);
        d.setExported(dependency.getExported());
        if (scope != null) {
          d.setScope(scope);
        }
        d.setOrder(i);
        ideModule.createChild(ProjectKeys.MODULE_DEPENDENCY, d);
        ModuleData targetModule = d.getTarget();
        if (targetModule.getId().isEmpty() && targetModule.getLinkedExternalProjectPath().isEmpty()) {
          orphanModules.add(targetModule.getExternalName());
        }
      }
