// 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.idea.maven.importing;

import com.intellij.externalSystem.ImportedLibraryProperties;
import com.intellij.externalSystem.ImportedLibraryType;
import com.intellij.java.library.MavenCoordinates;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.externalSystem.ExternalSystemModulePropertyManager;
import com.intellij.openapi.externalSystem.service.project.IdeModifiableModelsProvider;
import com.intellij.openapi.module.ModifiableModuleModel;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.impl.libraries.LibraryEx;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.PersistentLibraryKind;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.Strings;
import com.intellij.openapi.vfs.JarFileSystem;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.pom.java.LanguageLevel;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.maven.model.MavenArtifact;
import org.jetbrains.idea.maven.model.MavenConstants;
import org.jetbrains.idea.maven.project.MavenProject;
import org.jetbrains.idea.maven.project.MavenProjectsManager;
import org.jetbrains.idea.maven.statistics.MavenImportCollector;
import org.jetbrains.idea.maven.utils.MavenPathWrapper;
import org.jetbrains.idea.maven.utils.MavenUtil;
import org.jetbrains.idea.maven.utils.Url;
import org.jetbrains.jps.model.JpsElement;
import org.jetbrains.jps.model.java.JavaSourceRootType;
import org.jetbrains.jps.model.java.JpsJavaExtensionService;
import org.jetbrains.jps.model.module.JpsModuleSourceRootType;
import org.jetbrains.jps.model.serialization.SerializationConstants;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;

public class MavenRootModelAdapterLegacyImpl implements MavenRootModelAdapterInterface {

  private final MavenProject myMavenProject;
  private final ModifiableModuleModel myModuleModel;
  private final ModifiableRootModel myRootModel;

  private final MavenSourceFoldersModuleExtension myRootModelModuleExtension;

  private final Set<String> myOrderEntriesBeforeJdk = new HashSet<>();
  private volatile Map<String, Library> myLibrariesTable;

  public MavenRootModelAdapterLegacyImpl(@NotNull MavenProject p,
                                         @NotNull Module module,
                                         final IdeModifiableModelsProvider rootModelsProvider) {
    myMavenProject = p;
    myModuleModel = rootModelsProvider.getModifiableModuleModel();
    myRootModel = rootModelsProvider.getModifiableRootModel(module);

    myRootModelModuleExtension = myRootModel.getModuleExtension(MavenSourceFoldersModuleExtension.class);
    myRootModelModuleExtension.init(module, myRootModel);
  }

  @Override
  public void init(boolean isNewlyCreatedModule) {
    setupInitialValues(isNewlyCreatedModule);
    initContentRoots();
    initOrderEntries();
  }

  private void setupInitialValues(boolean newlyCreatedModule) {
    if (newlyCreatedModule || myRootModel.getSdk() == null) {
      myRootModel.inheritSdk();
    }
    if (newlyCreatedModule) {
      getCompilerExtension().setExcludeOutput(true);
    }
  }

  private void initContentRoots() {
    Url url = toUrl(myMavenProject.getDirectory());
    if (getContentRootFor(url) != null) return;
    myRootModel.addContentEntry(url.getUrl(), getMavenExternalSource());
  }

  private ContentEntry getContentRootFor(Url url) {
    for (ContentEntry e : myRootModel.getContentEntries()) {
      if (VfsUtilCore.isEqualOrAncestor(e.getUrl(), url.getUrl())) return e;
    }
    return null;
  }

  private void initOrderEntries() {
    boolean jdkProcessed = false;

    for (OrderEntry e : myRootModel.getOrderEntries()) {
      if (e instanceof ModuleSourceOrderEntry || e instanceof JdkOrderEntry) {
        jdkProcessed = true;
        continue;
      }

      if (e instanceof LibraryOrderEntry) {
        if (Registry.is("maven.always.remove.bad.entries")) {
          if (!isMavenLibrary((LibraryOrderEntry)e)) {
            MavenImportCollector.HAS_USER_ADDED_LIBRARY_DEP.log(myRootModel.getProject());
            continue;
          }
        }
        else {
          if (!isMavenLibrary(((LibraryOrderEntry)e).getLibrary())) {
            MavenImportCollector.HAS_USER_ADDED_LIBRARY_DEP.log(myRootModel.getProject());
            continue;
          }
        }
      }
      if (e instanceof ModuleOrderEntry) {
        Module m = ((ModuleOrderEntry)e).getModule();
        if (m != null &&
            !MavenProjectsManager.getInstance(myRootModel.getProject()).isMavenizedModule(m) &&
            ExternalSystemModulePropertyManager.getInstance(m).getExternalSystemId() == null) {
          MavenImportCollector.HAS_USER_ADDED_MODULE_DEP.log(myRootModel.getProject());
          continue;
        }
      }

      if (!jdkProcessed) {
        if (e instanceof ModuleOrderEntry) {
          myOrderEntriesBeforeJdk.add(((ModuleOrderEntry)e).getModuleName());
        }
        else if (e instanceof LibraryOrderEntry) {
          myOrderEntriesBeforeJdk.add(((LibraryOrderEntry)e).getLibraryName());
        }
      }

      myRootModel.removeOrderEntry(e);
    }
  }

  @Override
  public ModifiableRootModel getRootModel() {
    return myRootModel;
  }

  @Override
  public Module getModule() {
    return myRootModel.getModule();
  }

  @Override
  public <P extends JpsElement> void addSourceFolder(String path, final JpsModuleSourceRootType<P> rootType) {
    addSourceFolder(path, rootType, false, rootType.createDefaultProperties());
  }

  @Override
  public void addGeneratedJavaSourceFolder(String path, JavaSourceRootType rootType) {
    addSourceFolder(path, rootType, true, JpsJavaExtensionService.getInstance().createSourceRootProperties("", true));
  }

  private <P extends JpsElement> void addSourceFolder(@NotNull String path,
                                                      final @NotNull JpsModuleSourceRootType<P> rootType,
                                                      boolean ifNotEmpty,
                                                      final @NotNull P properties) {
    if (ifNotEmpty) {
      String[] childs = new File(toPath(path).getPath()).list();
      if (childs == null || childs.length == 0) return;
    }
    else {
      if (!exists(path)) return;
    }

    Url url = toUrl(path);
    myRootModelModuleExtension.addSourceFolder(url, rootType, properties);
  }

  @Override
  public boolean isAlreadyExcluded(File f) {
    String url = toUrl(f.getPath()).getUrl();
    return VfsUtilCore.isUnder(url, Arrays.asList(myRootModel.getExcludeRootUrls()));
  }

  private boolean exists(String path) {
    return Files.exists(Paths.get(toPath(path).getPath()));
  }

  @Override
  public void addExcludedFolder(String path) {
    unregisterAll(path, true, false);
    Url url = toUrl(path);
    ContentEntry e = getContentRootFor(url);
    if (e == null) return;
    if (e.getUrl().equals(url.getUrl())) return;
    e.addExcludeFolder(url.getUrl(), true);
  }

  @Override
  public void unregisterAll(String path, boolean under, boolean unregisterSources) {
    Url url = toUrl(path);

    for (ContentEntry eachEntry : myRootModel.getContentEntries()) {
      if (unregisterSources) {
        myRootModelModuleExtension.unregisterAll(url, under);
      }

      for (String excludedUrl : eachEntry.getExcludeFolderUrls()) {
        String ancestor = under ? url.getUrl() : excludedUrl;
        String child = under ? excludedUrl : url.getUrl();

        if (VfsUtilCore.isEqualOrAncestor(ancestor, child)) {
          eachEntry.removeExcludeFolder(excludedUrl);
        }
      }
      for (String outputUrl : getCompilerExtension().getOutputRootUrls(true)) {
        String ancestor = under ? url.getUrl() : outputUrl;
        String child = under ? outputUrl : url.getUrl();
        if (VfsUtilCore.isEqualOrAncestor(ancestor, child)) {
          getCompilerExtension().setExcludeOutput(false);
        }
      }
    }
  }

  @Override
  public boolean hasCollision(String sourceRootPath) {
    Url url = toUrl(sourceRootPath);

    for (ContentEntry eachEntry : myRootModel.getContentEntries()) {
      for (SourceFolder eachFolder : eachEntry.getSourceFolders()) {
        String ancestor = url.getUrl();
        String child = eachFolder.getUrl();
        if (VfsUtilCore.isEqualOrAncestor(ancestor, child) || VfsUtilCore.isEqualOrAncestor(child, ancestor)) {
          return true;
        }
      }

      for (String excludeUrl : eachEntry.getExcludeFolderUrls()) {
        String ancestor = url.getUrl();
        if (VfsUtilCore.isEqualOrAncestor(ancestor, excludeUrl) || VfsUtilCore.isEqualOrAncestor(excludeUrl, ancestor)) {
          return true;
        }
      }
    }

    return false;
  }

  @Override
  public void useModuleOutput(String production, String test) {
    getCompilerExtension().inheritCompilerOutputPath(false);
    if (Strings.isEmpty(production) && Strings.isEmpty(test)) {
      getCompilerExtension().inheritCompilerOutputPath(true);
    }
    else if (Strings.isEmpty(test)) {
      getCompilerExtension().setCompilerOutputPath(toUrl(production).getUrl());
      getCompilerExtension().setExcludeOutput(true);
    }
    else if (Strings.isEmpty(production)) {
      getCompilerExtension().setCompilerOutputPathForTests(toUrl(test).getUrl());
      getCompilerExtension().setExcludeOutput(true);
    }
    else {
      getCompilerExtension().setCompilerOutputPath(toUrl(production).getUrl());
      getCompilerExtension().setCompilerOutputPathForTests(toUrl(test).getUrl());
    }
  }

  private CompilerModuleExtension getCompilerExtension() {
    return myRootModel.getModuleExtension(CompilerModuleExtension.class);
  }

  private Url toUrl(String path) {
    return toPath(path).toUrl();
  }

  @Override
  public MavenPathWrapper toPath(String path) {
    return MavenUtil.toPath(myMavenProject, path);
  }

  @Override
  public void addModuleDependency(@NotNull String moduleName,
                                  @NotNull DependencyScope scope,
                                  boolean testJar) {
    myLibrariesTable = null;
    Module m = findModuleByName(moduleName);

    ModuleOrderEntry e;
    if (m != null) {
      e = myRootModel.addModuleOrderEntry(m);
    }
    else {
      e = ReadAction.compute(() -> myRootModel.addInvalidModuleEntry(moduleName));
    }

    e.setScope(scope);
    if (testJar) {
      e.setProductionOnTestDependency(true);
    }

    if (myOrderEntriesBeforeJdk.contains(moduleName)) {
      moveLastOrderEntryBeforeJdk();
    }
  }

  @Override
  public @Nullable Module findModuleByName(String moduleName) {
    return myModuleModel.findModuleByName(moduleName);
  }

  @Override
  public void addSystemDependency(MavenArtifact artifact, DependencyScope scope) {
    assert MavenConstants.SCOPE_SYSTEM.equals(artifact.getScope());

    String libraryName = artifact.getLibraryName();

    Library library = myRootModel.getModuleLibraryTable().getLibraryByName(libraryName);
    if (library == null) {
      library = myRootModel.getModuleLibraryTable().createLibrary(libraryName);
    }

    LibraryOrderEntry orderEntry = myRootModel.findLibraryOrderEntry(library);
    assert orderEntry != null;
    orderEntry.setScope(scope);

    Library.ModifiableModel modifiableModel = library.getModifiableModel();
    updateUrl(modifiableModel, OrderRootType.CLASSES, artifact, null, null, true);
    modifiableModel.commit();

    if (myOrderEntriesBeforeJdk.contains(libraryName)) {
      moveLastOrderEntryBeforeJdk();
    }
  }

  @Override
  public LibraryOrderEntry addLibraryDependency(MavenArtifact artifact,
                                                DependencyScope scope,
                                                IdeModifiableModelsProvider provider,
                                                MavenProject project) {
    assert !MavenConstants.SCOPE_SYSTEM.equals(
      artifact.getScope()); // System dependencies must be added ad module library, not as project wide library.

    myLibrariesTable = null;
    String libraryName = artifact.getLibraryName();

    Library library = provider.getLibraryByName(libraryName);
    if (library == null) {
      library = provider.createLibrary(libraryName, getMavenExternalSource());
    }
    Library.ModifiableModel libraryModel = provider.getModifiableLibraryModel(library);

    if (library.getExternalSource() == null) {
      ((LibraryEx.ModifiableModelEx)libraryModel).setExternalSource(getMavenExternalSource());
    }
    updateUrl(libraryModel, OrderRootType.CLASSES, artifact, null, null, true);
    updateUrl(libraryModel, OrderRootType.SOURCES, artifact, MavenExtraArtifactType.SOURCES, project, false);
    updateUrl(libraryModel, JavadocOrderRootType.getInstance(), artifact, MavenExtraArtifactType.DOCS, project, false);

    if (libraryModel != null) {
      LibraryEx.ModifiableModelEx model = (LibraryEx.ModifiableModelEx)libraryModel;
      PersistentLibraryKind<ImportedLibraryProperties> importedLibraryKind = ImportedLibraryType.Companion.getIMPORTED_LIBRARY_KIND();
      if (model.getKind() != importedLibraryKind) {
        model.setKind(importedLibraryKind);
        model.setProperties(new ImportedLibraryProperties(new MavenCoordinates(artifact.getGroupId(),
                                                                               artifact.getArtifactId(),
                                                                               artifact.getVersion(),
                                                                               artifact.getBaseVersion(),
                                                                               artifact.getPackaging(),
                                                                               artifact.getClassifier())));
      }
    }


    LibraryOrderEntry e = myRootModel.addLibraryEntry(library);
    e.setScope(scope);

    if (myOrderEntriesBeforeJdk.contains(libraryName)) {
      moveLastOrderEntryBeforeJdk();
    }

    return e;
  }

  private void moveLastOrderEntryBeforeJdk() {
    OrderEntry[] entries = myRootModel.getOrderEntries().clone();

    int i = entries.length - 1;
    while (i > 0 && (entries[i - 1] instanceof ModuleSourceOrderEntry || entries[i - 1] instanceof JdkOrderEntry)) {
      OrderEntry e = entries[i - 1];
      entries[i - 1] = entries[i];
      entries[i] = e;
      i--;
    }

    if (i < entries.length) {
      myRootModel.rearrangeOrderEntries(entries);
    }
  }

  private static void updateUrl(Library.ModifiableModel library,
                                OrderRootType type,
                                MavenArtifact artifact,
                                MavenExtraArtifactType artifactType,
                                MavenProject project,
                                boolean clearAll) {
    String classifier = null;
    String extension = null;

    if (artifactType != null) {
      Pair<String, String> result = project.getClassifierAndExtension(artifact, artifactType);
      classifier = result.first;
      extension = result.second;
    }


    String newPath = artifact.getPathForExtraArtifact(classifier, extension);
    String newUrl = VirtualFileManager.constructUrl(JarFileSystem.PROTOCOL, newPath) + JarFileSystem.JAR_SEPARATOR;

    boolean urlExists = false;

    for (String url : library.getUrls(type)) {
      if (newUrl.equals(url)) {
        urlExists = true;
        continue;
      }
      if (clearAll || (isRepositoryUrl(artifact, url) && !url.startsWith(newUrl))) {
        library.removeRoot(url, type);
      }
    }

    if (!urlExists) {
      library.addRoot(newUrl, type);
    }
  }

  private static boolean isRepositoryUrl(MavenArtifact artifact, String url) {
    return url.contains(artifact.getGroupId().replace('.', '/') +
                        '/' +
                        artifact.getArtifactId() +
                        '/' +
                        artifact.getBaseVersion() +
                        '/' +
                        artifact.getArtifactId() +
                        '-');
  }


  @Override
  public Library findLibrary(final @NotNull MavenArtifact artifact) {
    return getOrCreateLibrariesTable().get(artifact.getLibraryName());
  }

  private Map<String, Library> getOrCreateLibrariesTable() {
    Map<String, Library> table = myLibrariesTable;
    if (table == null) {
      Map<String, Library> temp = new HashMap<>();
      myRootModel.orderEntries().forEachLibrary(library -> {
        temp.put(library.getName(), library);
        return true;
      });
      myLibrariesTable = temp;
      return temp;
    }
    return table;
  }

  public static boolean isMavenLibrary(@Nullable Library library) {
    return library != null && MavenArtifact.isMavenLibrary(library.getName());
  }

  public static boolean isMavenLibrary(@Nullable LibraryOrderEntry entry) {
    return entry != null && MavenArtifact.isMavenLibrary(entry.getLibraryName());
  }

  public static ProjectModelExternalSource getMavenExternalSource() {
    return ExternalProjectSystemRegistry.getInstance().getSourceById(SerializationConstants.MAVEN_EXTERNAL_SOURCE_ID);
  }

  @Override
  public void setLanguageLevel(LanguageLevel level) {
    try {
      level = MavenImportUtil.adjustLevelAndNotify(myRootModel.getProject(), level);
      myRootModel.getModuleExtension(LanguageLevelModuleExtension.class).setLanguageLevel(level);
    }
    catch (IllegalArgumentException e) {
      //bad value was stored
    }
  }
}
