package jetbrains.mps.baseLanguage.unitTest.execution.client;

/*Generated by MPS */

import jetbrains.mps.logging.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.module.SRepository;
import java.util.List;
import jetbrains.mps.internal.collections.runtime.ListSequence;
import java.util.LinkedList;
import java.util.Set;
import org.jetbrains.mps.openapi.module.SModule;
import jetbrains.mps.internal.collections.runtime.SetSequence;
import java.util.HashSet;
import org.jetbrains.mps.openapi.module.SModuleReference;
import java.util.ArrayList;
import jetbrains.mps.baseLanguage.execution.api.Java_Command;
import org.jetbrains.mps.openapi.persistence.PersistenceFacade;
import jetbrains.mps.util.PathManager;
import jetbrains.mps.internal.collections.runtime.Sequence;
import java.util.LinkedHashSet;
import java.io.File;
import jetbrains.mps.string.Strings;

/*package*/ class CPCalculator {
  private static final Logger LOG = Logger.getLogger(CPCalculator.class);
  private static final String UNIT_TEST_LAUNCHER_MODULE = "33f214de-6dce-4396-83c7-640823b7c525(jetbrains.mps.baselanguage.unitTest.launcher)";

  @NotNull
  private final TestsWithParametersAndConfiguration mySettings;
  @NotNull
  private final TestsWithParameters myTestsWithParams;
  @NotNull
  private final SRepository myRepo;

  public CPCalculator(@NotNull final TestsWithParametersAndConfiguration settings) {
    mySettings = settings;
    myTestsWithParams = settings.myTests2Run;
    myRepo = settings.myRepo;
  }

  public List<String> calculate() {
    boolean doINeedMPS = myTestsWithParams.getParameters().needsMPS();
    List<String> classpath = (doINeedMPS ? calcForPlatformWithMPS() : calcForPlatformWithoutMPS());
    return ListSequence.fromList(myTestsWithParams.getParameters().getClassPath()).union(ListSequence.fromList(classpath)).toList();
  }

  private List<String> calcForPlatformWithoutMPS() {
    final List<String> classpath = ListSequence.fromList(new LinkedList<String>());
    // when no MPS is started, we just build a regular Java classpath with everything test classes may need.
    myRepo.getModelAccess().runReadAction(() -> {
      Set<SModule> uniqueModules = SetSequence.fromSet(new HashSet<SModule>());
      List<SModuleReference> requiredModules = new ArrayList<SModuleReference>(myTestsWithParams.getRequiredModules());
      requiredModules.add(MODULE_WITH_EXECUTORS());
      for (SModuleReference testModule : requiredModules) {
        SModule module = testModule.resolve(myRepo);
        if (module != null) {
          SetSequence.fromSet(uniqueModules).addElement(module);
        } else {
          String m = String.format("No test module %s is available, execution classpath may be invalid.", testModule.getModuleName());
          if (LOG.isWarningLevel()) {
            LOG.warning(m);
          }
        }
      }
      ListSequence.fromList(classpath).addSequence(ListSequence.fromList(Java_Command.getClasspath(uniqueModules)));
    });
    return classpath;
  }

  private List<String> calcForPlatformWithMPS() {
    final SModuleReference moduleWithExecutors = PersistenceFacade.getInstance().createModuleReference(UNIT_TEST_LAUNCHER_MODULE);
    final List<String> classpath = ListSequence.fromList(new LinkedList<String>());
    if (PathManager.isFromSources()) {
      ListSequence.fromList(classpath).addElement(PathManager.getLauncherClassPathEntry());
    }
    // WithPlatformTestExecutor starts IDEA, therefore needs it in CP
    ListSequence.fromList(classpath).addSequence(ListSequence.fromList(collectFromLibFolder()).distinct());
    // Module classpath would get managed by IdeaEnvironment based on set of modules to load
    //  
    // next is to ensure TestExecutor is loaded. Though it's part of execution plugin, it's a regular mps module
    // and is managed by MPS classloader once MPS starts, while we need it first, to start MPS.
    myRepo.getModelAccess().runReadAction(() -> {
      SModule m = moduleWithExecutors.resolve(myRepo);
      if (m != null) {
        ListSequence.fromList(classpath).addSequence(ListSequence.fromList(Java_Command.getClasspath(Sequence.<SModule>singleton(m))));
      } else {
        String msg = String.format("No test module %s is available, execution classpath may be invalid.", moduleWithExecutors.getModuleName());
        // we likely to fail anyway, error is better than warn
        if (LOG.isErrorLevel()) {
          LOG.error(msg);
        }
      }
    });
    return classpath;
  }

  private List<String> collectFromLibFolder() {
    LinkedHashSet<String> libPaths = new LinkedHashSet<String>();
    // mps lib and platform lib may be the same
    libPaths.add(PathManager.getPlatformLibPath());
    libPaths.add(PathManager.getLibPath());
    libPaths.add(PathManager.getLibPath() + File.separator + "mpsant");
    libPaths.add(PathManager.getLibExtPath());
    List<String> rv = new ArrayList<String>();
    for (String lp : libPaths) {
      File libDir = new File(lp);
      if (!(libDir.isDirectory())) {
        continue;
      }
      for (File f : libDir.listFiles()) {
        if (isJarOrZip(f)) {
          rv.add(f.getPath());
        }
      }
      // don't get into nested dir. though this is different e.g.
      // from ant tasks (see MPSClasspathUtil#gatherAllClassesAndJarsUnder()
      // this is how it was with ClassloaderUtil.addIDEALibraries that used to be here.
    }
    return rv;
  }

  private List<String> collectFromPreInstalledPluginsFolder() {
    List<String> result = ListSequence.fromList(new ArrayList<String>());
    // XXX why pre-installed only, and not any locally-deployed (getPluginsPath())?
    // Why not MPS's PathManager but IDEA's?
    File preinstalledFolder = new File(com.intellij.openapi.application.PathManager.getPreInstalledPluginsPath());
    final File[] pluginFiles = preinstalledFolder.listFiles();
    if (pluginFiles != null) {
      for (final File pluginFile : pluginFiles) {
        if (!(isJarOrZip(pluginFile))) {
          File classesDir = new File(pluginFile, "classes");
          ListSequence.fromList(result).addElement(classesDir.getAbsolutePath());
          File libDir = new File(pluginFile, "lib");
          ListSequence.fromList(result).addSequence(ListSequence.fromList(gatherAllJarsUnderRoot(libDir)));
        }
      }
    }
    ListSequence.fromList(result).addSequence(ListSequence.fromList(gatherAllJarsUnderRoot(preinstalledFolder)));
    return result;
  }

  private List<String> gatherAllJarsUnderRoot(File root) {
    List<String> res = ListSequence.fromList(new ArrayList<String>());
    if (!(root.exists())) {
      return res;
    }
    File[] children = root.listFiles();
    if (children != null) {
      for (final File childFile : children) {
        if (isJarOrZip(childFile)) {
          ListSequence.fromList(res).addElement(childFile.getAbsolutePath());
        }
      }
    }

    return res;
  }

  @SuppressWarnings(value = {"HardCodedStringLiteral"})
  private static boolean isJarOrZip(File file) {
    if (file.isDirectory()) {
      return false;
    }
    final String name = file.getName();
    return Strings.endsWithIgnoreCase(name, ".jar") || Strings.endsWithIgnoreCase(name, ".zip");
  }

  @NotNull
  /*package*/ static SModuleReference MODULE_WITH_EXECUTORS() {
    // next module used to be in defaults of TestParameters, don't see a reason why can't do it here, though.
    // With classpath, we have to
    // ensure *TestExecutor classes get loaded (unitTest.execution.server package). The best approach in that case
    // would be for TestParameters to tell set of required modules (instead of/in addition to classpath list)
    // as it's TestParameters class that knows specific contributor class location, however, such a change would
    // require changes in TestParameters#comprises() logic, which needs a thorough refactoring to get rid of
    // Class<> in getExecutorClass() anyway.
    // The reason I put it here is that I lean towards no executorClass in TestParameters at all, so that
    // this command would pick executor class based on information whether need to start MPS or not, and therfore
    // would add relevant module into classpath here anyway.

    return PersistenceFacade.getInstance().createModuleReference("f618e99a-2641-465c-bb54-31fe76f9e285(jetbrains.mps.baseLanguage.unitTest.execution)");
  }
}
