// 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.eclipse;

import com.intellij.openapi.components.ExpandMacroToPathMap;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.*;
import java.util.*;
import java.util.jar.Manifest;
import java.util.regex.PatternSyntaxException;

public abstract class AbstractEclipseClasspathReader<T> {
  protected final String myRootPath;
  protected final @Nullable List<String> myCurrentRoots;
  protected final @Nullable Set<String> myModuleNames;

  public AbstractEclipseClasspathReader(@NotNull String rootPath, @Nullable List<String> currentRoots, @Nullable Set<String> moduleNames) {
    myRootPath = FileUtil.toSystemIndependentName(rootPath);
    myCurrentRoots = currentRoots;
    myModuleNames = moduleNames;
  }

  protected abstract String prepareValidUrlInsideJar(String url);

  protected abstract void addNamedLibrary(T rootModel,
                                          Collection<? super String> unknownLibraries,
                                          boolean exported,
                                          String name,
                                          boolean applicationLevel);

  protected abstract void addInvalidModuleEntry(T rootModel, boolean exported, String moduleName);

  protected abstract void setUpModuleJdk(T rootModel,
                                         Collection<? super String> unknownJdks,
                                         EclipseModuleManager eclipseModuleManager,
                                         String jdkName);

  public abstract void setupOutput(T rootModel, String path);

  protected abstract void addSourceFolder(T rootModel, String srcUrl, boolean testFolder);

  protected abstract void addSourceFolderToCurrentContentRoot(T rootModel, String srcUrl, boolean testFolder);

  protected abstract void addJUnitDefaultLib(T rootModel, String junitName, ExpandMacroToPathMap macroMap);

  protected abstract void addModuleLibrary(T rootModel,
                                           Element element,
                                           boolean exported,
                                           String libName,
                                           String url,
                                           String srcUrl, String nativeRoot, ExpandMacroToPathMap macroMap);

  protected abstract String expandEclipsePath2Url(T rootModel, String path);

  protected abstract Set<String> getDefinedCons();

  protected abstract int rearrange(T rootModel);

  protected void readClasspathEntry(T rootModel,
                                    final Collection<? super String> unknownLibraries,
                                    Collection<? super String> unknownJdks,
                                    Set<? super String> refsToModules,
                                    final String testPattern,
                                    Element element, int index,
                                    @Nullable EclipseModuleManager eclipseModuleManager,
                                    final ExpandMacroToPathMap macroMap,
                                    final Set<? super String> libs) throws ConversionException {
    String kind = element.getAttributeValue(EclipseXml.KIND_ATTR);
    if (kind == null) {
      throw new ConversionException("Missing classpathentry/@kind");
    }

    String path = element.getAttributeValue(EclipseXml.PATH_ATTR);
    if (path == null) {
      throw new ConversionException("Missing classpathentry/@path");
    }

    final boolean exported = EclipseXml.TRUE_VALUE.equals(element.getAttributeValue(EclipseXml.EXPORTED_ATTR));

    if (kind.equals(EclipseXml.SRC_KIND)) {
      if (path.startsWith("/")) {
        final String moduleName = path.substring(1);
        refsToModules.add(moduleName);
        addInvalidModuleEntry(rootModel, exported, moduleName);
      }
      else {
        String srcUrl = pathToUrl(getPathRelativeToRoot(path));
        boolean isTestFolder;
        try {
          isTestFolder = !StringUtil.isEmpty(testPattern) && path.matches(testPattern);
        }
        catch (PatternSyntaxException e) {
          isTestFolder = false;
        }
        String linked = expandLinkedResourcesPath(myRootPath, macroMap, path);
        if (linked == null) {
          addSourceFolderToCurrentContentRoot(rootModel, srcUrl, isTestFolder);
        }
        else {
          srcUrl = prepareValidUrlInsideJar(pathToUrl(linked));
          if (eclipseModuleManager != null) {
            eclipseModuleManager.registerEclipseLinkedSrcVarPath(srcUrl, path);
          }
          addSourceFolder(rootModel, srcUrl, isTestFolder);
        }

        if (eclipseModuleManager != null) {
          eclipseModuleManager.setExpectedModuleSourcePlace(rearrange(rootModel));
          eclipseModuleManager.registerSrcPlace(srcUrl, index);
        }
      }
    }
    else if (kind.equals(EclipseXml.OUTPUT_KIND)) {
      String output = getPathRelativeToRoot(path);
      String linked = expandLinkedResourcesPath(myRootPath, macroMap, path);
      if (linked != null) {
        output = linked;
        if (eclipseModuleManager != null) {
          eclipseModuleManager.registerEclipseLinkedVarPath(pathToUrl(output), path);
        }
      }
      setupOutput(rootModel, output);
    }
    else if (kind.equals(EclipseXml.LIB_KIND)) {
      String linked = expandLinkedResourcesPath(myRootPath, macroMap, path);
      String url;
      if (linked != null) {
        url = prepareValidUrlInsideJar(pathToUrl(linked));
        if (eclipseModuleManager != null) {
          eclipseModuleManager.registerEclipseLinkedVarPath(url, path);
        }
      }
      else {
        url = expandEclipsePath2Url(rootModel, path);
      }

      if (eclipseModuleManager != null) {
        eclipseModuleManager.registerEclipseLibUrl(url);
      }

      final String sourcePath = element.getAttributeValue(EclipseXml.SOURCEPATH_ATTR);
      String srcUrl = null;
      if (sourcePath != null) {
        final String linkedSrc = expandLinkedResourcesPath(myRootPath, macroMap, sourcePath);
        if (linkedSrc != null) {
          srcUrl = prepareValidUrlInsideJar(pathToUrl(linkedSrc));
          if (eclipseModuleManager != null) {
            eclipseModuleManager.registerEclipseLinkedSrcVarPath(srcUrl, sourcePath);
          }
        }
        else {
          srcUrl = expandEclipsePath2Url(rootModel, sourcePath);
        }
      }

      String nativeRoot = getNativeLibraryRoot(element);
      if (nativeRoot != null) {
        nativeRoot = expandEclipsePath2Url(rootModel, nativeRoot);
      }

      addModuleLibrary(rootModel, element, exported, getPresentableName(path, libs), url, srcUrl, nativeRoot, macroMap);
    }
    else if (kind.equals(EclipseXml.VAR_KIND)) {
      int slash = path.indexOf("/");
      if (slash == 0) {
        throw new ConversionException("Incorrect 'classpathentry/var@path' format");
      }

      final String libName = getPresentableName(path, libs);

      String url = eclipseVariabledPath2Url(macroMap, path, 0);
      if (eclipseModuleManager != null) {
        eclipseModuleManager.registerEclipseVariablePath(url, path);
      }
      final String srcPathAttr = element.getAttributeValue(EclipseXml.SOURCEPATH_ATTR);
      String srcUrl = null;
      if (srcPathAttr != null) {
        srcUrl = eclipseVariabledPath2Url(macroMap, srcPathAttr, srcVarStart(srcPathAttr));
        if (eclipseModuleManager != null) {
          eclipseModuleManager.registerEclipseSrcVariablePath(srcUrl, srcPathAttr);
        }
      }
      String nativeRoot = getNativeLibraryRoot(element);
      if (nativeRoot != null) {
        nativeRoot = expandEclipsePath2Url(rootModel, nativeRoot);
      }
      addModuleLibrary(rootModel, element, exported, libName, url, srcUrl, nativeRoot, macroMap);
    }
    else if (kind.equals(EclipseXml.CON_KIND)) {
      if (path.equals(EclipseXml.ECLIPSE_PLATFORM)) {
        readRequiredBundles(rootModel, refsToModules);
        addNamedLibrary(rootModel, unknownLibraries, exported, IdeaXml.ECLIPSE_LIBRARY, true);
      }
      else if (path.startsWith(EclipseXml.JRE_CONTAINER)) {

        final String jdkName = getLastPathComponent(path);
        setUpModuleJdk(rootModel, unknownJdks, eclipseModuleManager, jdkName);
      }
      else if (path.startsWith(EclipseXml.USER_LIBRARY)) {
        addNamedLibrary(rootModel, unknownLibraries, exported, getPresentableName(path), false);
      }
      else if (path.startsWith(EclipseXml.JUNIT_CONTAINER)) {
        final String junitName = IdeaXml.JUNIT + getPresentableName(path);
        addJUnitDefaultLib(rootModel, junitName, macroMap);
      }
      else {
        Set<String> registeredCons = getDefinedCons();
        if (registeredCons.contains(path)) {
          if (eclipseModuleManager != null) {
            eclipseModuleManager.registerCon(path);
            eclipseModuleManager.registerSrcPlace(path, index);
          }
        }
        else {
          if (eclipseModuleManager != null) {
            eclipseModuleManager.registerUnknownCons(path);
          }
          addNamedLibrary(rootModel, new ArrayList<>(), exported, path, true);
        }
      }
    }
    else {
      throw new ConversionException("Unknown classpathentry/@kind: " + kind);
    }
  }

  private String getPathRelativeToRoot(String path) {
    return path.isEmpty() ? myRootPath : (myRootPath + "/" + path);
  }

  public static String getNativeLibraryRoot(Element element) {
    final Element attributes = element.getChild(EclipseXml.ATTRIBUTES_TAG);
    if (attributes != null) {
      for (Element attributeElement : attributes.getChildren(EclipseXml.ATTRIBUTE_TAG)) {
        if (EclipseXml.DLL_LINK.equals(attributeElement.getAttributeValue(EclipseXml.NAME_ATTR))) {
          return attributeElement.getAttributeValue(EclipseXml.VALUE_ATTR);
        }
      }
    }
    return null;
  }

  public static int srcVarStart(String srcPath) {
    return srcPath.startsWith("/") ? 1 : 0;
  }

  public static @NotNull String getPresentableName(@NotNull String path) {
    return getPresentableName(path, null);
  }

  public static @NotNull String getPresentableName(@NotNull String path, Set<? super String> names) {
    String pathComponent = getLastPathComponent(path);
    if (pathComponent != null && names != null && !names.add(pathComponent)) {
      return path;
    }
    else {
      return pathComponent == null ? path : pathComponent;
    }
  }

  public static @Nullable String getLastPathComponent(final String path) {
    final int idx = path.lastIndexOf('/');
    return idx < 0 || idx == path.length() - 1 ? null : path.substring(idx + 1);
  }

  public static @NotNull String getVariableRelatedPath(@NotNull String var, String path) {
    return "$" + var + "$" + (path == null ? "" : "/" + path);
  }

  protected static @NotNull @NonNls String pathToUrl(@NotNull String path) {
    return "file://" + FileUtil.toSystemIndependentName(path);
  }

  public static EPathVariable createEPathVariable(String pathAttr, int varStart) {
    int slash = pathAttr.indexOf("/", varStart);
    if (slash > 0) {
      return new EPathVariable(pathAttr.substring(varStart, slash), pathAttr.substring(slash + 1));
    }
    else {
      return new EPathVariable(pathAttr.substring(varStart), null);
    }
  }

  @SuppressWarnings("SpellCheckingInspection")
  protected String eclipseVariabledPath2Url(ExpandMacroToPathMap pathMap, String path, int varStart) {
    EPathVariable var = createEPathVariable(path, varStart);
    return prepareValidUrlInsideJar(pathMap.substitute(pathToUrl(getVariableRelatedPath(var.myVariable, var.myRelatedPath)), SystemInfo.isFileSystemCaseSensitive));
  }

  public static @Nullable String expandLinkedResourcesPath(String rootPath, final ExpandMacroToPathMap macroMap,
                                                           final String path) {
    final EclipseProjectFinder.LinkedResource linkedResource = EclipseProjectFinder.findLinkedResource(rootPath, path);
    if (linkedResource != null) {
      if (linkedResource.containsPathVariable()) {
        final String toPathVariableFormat =
          getVariableRelatedPath(linkedResource.getVariableName(), linkedResource.getRelativeToVariablePath());
        return macroMap.substitute(toPathVariableFormat, SystemInfo.isFileSystemCaseSensitive);
      }
      return linkedResource.getLocation();
    }
    return null;
  }


  protected void readRequiredBundles(T rootModel, Set<? super String> refsToModules) throws ConversionException {
    if (myModuleNames == null) {
      return;
    }

    final File manifestFile = new File(myRootPath, "META-INF/MANIFEST.MF");
    if (!manifestFile.exists()) {
      return;
    }

    try (InputStream in = new BufferedInputStream(new FileInputStream(manifestFile))) {
      final Manifest manifest = new Manifest(in);
      final String attributes = manifest.getMainAttributes().getValue("Require-Bundle");
      if (!StringUtil.isEmpty(attributes)) {
        final StringTokenizer tokenizer = new StringTokenizer(attributes, ",");
        while (tokenizer.hasMoreTokens()) {
          String bundle = tokenizer.nextToken().trim();
          if (!bundle.isEmpty()) {
            final int constraintIndex = bundle.indexOf(';');
            if (constraintIndex != -1) {
              bundle = bundle.substring(0, constraintIndex).trim();
            }

            if (myModuleNames.contains(bundle)) {
              refsToModules.add(bundle);
              addInvalidModuleEntry(rootModel, false, bundle);
            }
          }
        }
      }
    }
    catch (IOException e) {
      throw new ConversionException(e.getMessage());
    }
  }

  public static class EPathVariable {
    private final @NotNull String myVariable;
    private final String myRelatedPath;

    protected EPathVariable(@NotNull String variable, final String relatedPath) {
      myVariable = variable;
      myRelatedPath = relatedPath;
    }

    public @NotNull String getVariable() {
      return myVariable;
    }

    public String getRelatedPath() {
      return myRelatedPath;
    }
  }
}
