// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.externalSystem.service.project;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.ExternalSystemManager;
import com.intellij.openapi.externalSystem.ExternalSystemModulePropertyManager;
import com.intellij.openapi.externalSystem.model.ProjectSystemId;
import com.intellij.openapi.externalSystem.service.project.manage.ExternalProjectsManagerImpl;
import com.intellij.openapi.externalSystem.settings.AbstractExternalSystemSettings;
import com.intellij.openapi.externalSystem.settings.ExternalProjectSettings;
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.configuration.ModulesConfigurator;
import com.intellij.openapi.roots.ui.configuration.ProjectStructureConfigurable;
import com.intellij.openapi.roots.ui.configuration.projectRoot.ModuleStructureExtension;
import com.intellij.openapi.roots.ui.configuration.projectRoot.StructureConfigurableContext;
import com.intellij.openapi.ui.MasterDetailsComponent;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * @author Vladislav.Soroka
 */
public final class ExternalModuleStructureExtension extends ModuleStructureExtension {

  private static final Logger LOG = Logger.getInstance(ExternalModuleStructureExtension.class);
  private @Nullable Project myProject;
  private boolean isExternalSystemsInvolved;
  private @Nullable Map<String, Pair<ProjectSystemId, ExternalProjectSettings>> myExternalProjectsToRestore;
  private @Nullable Map<String, ProjectSystemId> myOrphanProjectsCandidates;

  @Override
  public void reset(Project project) {
    if (project == null) return;
    myProject = project;
    isExternalSystemsInvolved = false;
    myExternalProjectsToRestore = getLinkedProjects(project);
    myOrphanProjectsCandidates = new HashMap<>();
  }

  @Override
  public void disposeUIResources() {
    try {
      if (isExternalSystemsInvolved) {
        assert myOrphanProjectsCandidates != null;
        assert myProject != null;
        if (myExternalProjectsToRestore != null) {
          for (Pair<ProjectSystemId, ExternalProjectSettings> settingsPair : myExternalProjectsToRestore.values()) {
            AbstractExternalSystemSettings settings = ExternalSystemApiUtil.getSettings(myProject, settingsPair.first);
            String rootProjectPath = settingsPair.second.getExternalProjectPath();
            if (settings.getLinkedProjectSettings(rootProjectPath) == null) {
              //noinspection unchecked
              settings.linkProject(settingsPair.second);
            }
            myOrphanProjectsCandidates.remove(rootProjectPath);
          }
        }

        ModulesConfigurator modulesConfigurator = getModulesConfigurator(myProject);
        if (modulesConfigurator != null) {
          for (Map.Entry<String, ProjectSystemId> entry : myOrphanProjectsCandidates.entrySet()) {
            String rootProjectPath = entry.getKey();
            if (StringUtil.isNotEmpty(rootProjectPath)) {
              unlinkProject(myProject, entry.getValue(), rootProjectPath);
            }
          }
        }
      }
    }
    catch (Throwable e) {
      LOG.warn(e);
    }
    finally {
      myProject = null;
      myExternalProjectsToRestore = null;
      myOrphanProjectsCandidates = null;
    }
  }

  @Override
  public boolean addModuleNodeChildren(Module module, MasterDetailsComponent.MyNode moduleNode, Runnable treeNodeNameUpdater) {
    String systemIdString = ExternalSystemModulePropertyManager.getInstance(module).getExternalSystemId();
    if (StringUtil.isNotEmpty(systemIdString)) {
      isExternalSystemsInvolved = true;
      String rootProjectPath = ExternalSystemApiUtil.getExternalRootProjectPath(module);
      if (myOrphanProjectsCandidates != null && StringUtil.isNotEmpty(rootProjectPath)) {
        myOrphanProjectsCandidates.put(rootProjectPath, new ProjectSystemId(systemIdString));
      }
    }
    return false;
  }

  @Override
  public void moduleRemoved(Module module) {
    String systemIdString = ExternalSystemModulePropertyManager.getInstance(module).getExternalSystemId();
    if (StringUtil.isEmpty(systemIdString)) return;

    String rootProjectPath = ExternalSystemApiUtil.getExternalRootProjectPath(module);
    if (StringUtil.isEmpty(rootProjectPath)) return;

    Project project = module.getProject();
    ModulesConfigurator modulesConfigurator = getModulesConfigurator(project);
    if (modulesConfigurator == null) return;

    for (Module m : modulesConfigurator.getModules()) {
      if (m != module && rootProjectPath.equals(ExternalSystemApiUtil.getExternalRootProjectPath(m))) {
        return;
      }
    }

    ProjectSystemId systemId = new ProjectSystemId(systemIdString);
    ExternalSystemApiUtil.getSettings(project, systemId).unlinkExternalProject(rootProjectPath);
    assert myOrphanProjectsCandidates != null;
    myOrphanProjectsCandidates.put(rootProjectPath, systemId);
    isExternalSystemsInvolved = true;
  }

  @Override
  public void afterModelCommit() {
    if (myProject == null) return;
    myExternalProjectsToRestore = getLinkedProjects(myProject);
  }

  private static @Nullable ModulesConfigurator getModulesConfigurator(Project project) {
    if (ApplicationManager.getApplication().isHeadlessEnvironment()) return null;
    final ProjectStructureConfigurable structureConfigurable = ProjectStructureConfigurable.getInstance(project);
    StructureConfigurableContext context = structureConfigurable.isUiInitialized() ? structureConfigurable.getContext() : null;
    return context != null ? context.getModulesConfigurator() : null;
  }

  private static Map<String, Pair<ProjectSystemId, ExternalProjectSettings>> getLinkedProjects(Project project) {
    Map<String, Pair<ProjectSystemId, ExternalProjectSettings>> result = new HashMap<>();
    for (ExternalSystemManager<?, ?, ?, ?, ?> manager : ExternalSystemManager.EP_NAME.getIterable()) {
      ProjectSystemId systemId = manager.getSystemId();
      AbstractExternalSystemSettings systemSettings = ExternalSystemApiUtil.getSettings(project, systemId);
      Collection projectsSettings = systemSettings.getLinkedProjectsSettings();
      for (Object settings : projectsSettings) {
        if (settings instanceof ExternalProjectSettings projectSettings) {
          result.put(projectSettings.getExternalProjectPath(), Pair.create(systemId, projectSettings));
        }
      }
    }

    return result;
  }

  private static void unlinkProject(@NotNull Project project, ProjectSystemId systemId, String rootProjectPath) {
    ExternalSystemApiUtil.getLocalSettings(project, systemId).forgetExternalProjects(Collections.singleton(rootProjectPath));
    ExternalSystemApiUtil.getSettings(project, systemId).unlinkExternalProject(rootProjectPath);
    ExternalProjectsManagerImpl.getInstance(project).forgetExternalProjectData(systemId, rootProjectPath);
  }
}
