// Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
// Licensed under the Apache License, Version 2.0 (see LICENSE).

package com.twitter.intellij.pants.service;

import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessOutput;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.model.ExternalSystemException;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.util.Consumer;
import com.intellij.util.PathUtil;
import com.intellij.util.containers.ContainerUtil;
import com.twitter.intellij.pants.PantsBundle;
import com.twitter.intellij.pants.PantsExecutionException;
import com.twitter.intellij.pants.metrics.PantsMetrics;
import com.twitter.intellij.pants.model.IJRC;
import com.twitter.intellij.pants.model.PantsCompileOptions;
import com.twitter.intellij.pants.model.PantsExecutionOptions;
import com.twitter.intellij.pants.settings.PantsExecutionSettings;
import com.twitter.intellij.pants.util.PantsUtil;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class PantsCompileOptionsExecutor {
  protected static final Logger LOG = Logger.getInstance(PantsCompileOptionsExecutor.class);
  public static final int PROJECT_NAME_LIMIT = 200;

  private final List<Process> myProcesses = ContainerUtil.createConcurrentList();

  private final PantsCompileOptions myOptions;
  private final File myBuildRoot;
  private final boolean myResolveSourcesAndDocsForJars;
  private final Optional<Integer> myIncrementalImportDepth;

  @NotNull
  public static PantsCompileOptionsExecutor create(
    @NotNull String projectRootPath,
    @Nullable PantsExecutionSettings executionOptions
  ) throws ExternalSystemException {
    if (executionOptions == null) {
      throw new ExternalSystemException("No execution options for " + projectRootPath);
    }
    PantsCompileOptions options = new MyPantsCompileOptions(projectRootPath, executionOptions);

    Optional<File> buildRoot = PantsUtil.findBuildRoot(new File(options.getExternalProjectPath()));
    if (!buildRoot.isPresent() || !buildRoot.get().exists()) {
      throw new ExternalSystemException(PantsBundle.message("pants.error.no.pants.executable.by.path", options.getExternalProjectPath()));
    }
    return new PantsCompileOptionsExecutor(
      buildRoot.get(),
      options,
      executionOptions.isLibsWithSourcesAndDocs(),
      executionOptions.incrementalImportDepth()
    );
  }

  @NotNull
  @TestOnly
  public static PantsCompileOptionsExecutor createMock() {
    return new PantsCompileOptionsExecutor(
      new File("/"),
      new MyPantsCompileOptions("", PantsExecutionSettings.createDefault()),
      true,
      Optional.of(1)
    ) {
    };
  }

  private PantsCompileOptionsExecutor(
    @NotNull File buildRoot,
    @NotNull PantsCompileOptions compilerOptions,
    boolean resolveSourcesAndDocsForJars,
    @NotNull Optional<Integer> incrementalImportDepth
  ) {
    myBuildRoot = buildRoot;
    myOptions = compilerOptions;
    myResolveSourcesAndDocsForJars = resolveSourcesAndDocsForJars;
    myIncrementalImportDepth = incrementalImportDepth;
  }

  public String getProjectRelativePath() {
    return PantsUtil.getRelativeProjectPath(getBuildRoot(), getProjectPath()).get();
  }

  @NotNull
  public Optional<Integer> getIncrementalImportDepth() {
    return myIncrementalImportDepth;
  }

  @NotNull
  public File getBuildRoot() {
    return myBuildRoot;
  }

  public String getProjectPath() {
    return myOptions.getExternalProjectPath();
  }

  @NotNull
  public String getProjectDir() {
    final File projectFile = new File(getProjectPath());
    final File projectDir = projectFile.isDirectory() ? projectFile : FileUtil.getParentFile(projectFile);
    return projectDir != null ? projectDir.getAbsolutePath() : projectFile.getAbsolutePath();
  }

  @NotNull
  @Nls
  public String getDefaultProjectName() {
    final String buildRootName = getBuildRoot().getName();
    List<String> buildRootPrefixedSpecs = myOptions.getSelectedTargetSpecs().stream()
      .map(s -> buildRootName + File.separator + s)
      .collect(Collectors.toList());
    String candidateName = String.join("__", buildRootPrefixedSpecs).replaceAll(File.separator, ".");
    return candidateName.substring(0, Math.min(PROJECT_NAME_LIMIT, candidateName.length()));
  }

  @NotNull
  @Nls
  public String getRootModuleName() {
    if (PantsUtil.isExecutable(myOptions.getExternalProjectPath())) {
      //noinspection ConstantConditions
      return PantsUtil.fileNameWithoutExtension(VfsUtil.extractFileName(myOptions.getExternalProjectPath()));
    }
    return getProjectRelativePath();
  }

  @NotNull
  public PantsCompileOptions getOptions() {
    return myOptions;
  }

  @NotNull
  public String loadProjectStructure(
    @NotNull Consumer<String> statusConsumer,
    @Nullable ProcessAdapter processAdapter
  ) throws IOException, ExecutionException {
    if (PantsUtil.isExecutable(getProjectPath())) {
      return loadProjectStructureFromScript(getProjectPath(), statusConsumer, processAdapter);
    }
    else {
      return loadProjectStructureFromTargets(statusConsumer, processAdapter);
    }
  }

  @NotNull
  private static String loadProjectStructureFromScript(
    @NotNull String scriptPath,
    @NotNull Consumer<String> statusConsumer,
    @Nullable ProcessAdapter processAdapter
  ) throws IOException, ExecutionException {
    final GeneralCommandLine commandLine = PantsUtil.defaultCommandLine(scriptPath);
    commandLine.setExePath(scriptPath);
    statusConsumer.consume("Executing " + PathUtil.getFileName(scriptPath));
    final ProcessOutput processOutput = PantsUtil.getCmdOutput(commandLine, processAdapter);
    if (processOutput.checkSuccess(LOG)) {
      return processOutput.getStdout();
    }
    else {
      throw new PantsExecutionException("Failed to update the project!", scriptPath, processOutput);
    }
  }

  @NotNull
  private String loadProjectStructureFromTargets(
    @NotNull Consumer<String> statusConsumer,
    @Nullable ProcessAdapter processAdapter
  ) throws IOException, ExecutionException {
    final File outputFile = FileUtil.createTempFile("pants_depmap_run", ".out");
    final GeneralCommandLine command = getPantsExportCommand(outputFile, statusConsumer);
    statusConsumer.consume("Resolving dependencies...");
    PantsMetrics.markExportStart();
    final ProcessOutput processOutput = getProcessOutput(command);
    PantsMetrics.markExportEnd();
    if (processOutput.getStdout().contains("no such option")) {
      throw new ExternalSystemException("Pants doesn't have necessary APIs. Please upgrade your pants!");
    }
    if (processOutput.checkSuccess(LOG)) {
      return FileUtil.loadFile(outputFile);
    }
    else {
      throw new PantsExecutionException("Failed to update the project!", command.getCommandLineString("pants"), processOutput);
    }
  }

  private ProcessOutput getProcessOutput(
    @NotNull GeneralCommandLine command
  ) throws ExecutionException {
    final Process process = command.createProcess();
    myProcesses.add(process);
    final ProcessOutput processOutput = PantsUtil.getCmdOutput(process, command.getCommandLineString(), null);
    myProcesses.remove(process);
    return processOutput;
  }

  @NotNull
  private GeneralCommandLine getPantsExportCommand(final File outputFile, @NotNull Consumer<String> statusConsumer)
    throws IOException {
    final GeneralCommandLine commandLine = PantsUtil.defaultCommandLine(getProjectPath());

    // Grab the import stage pants rc file for IntelliJ.
    Optional<String> rcArg = IJRC.getImportPantsRc(commandLine.getWorkDirectory().getPath());
    rcArg.ifPresent(commandLine::addParameter);

    final File targetSpecsFile = FileUtil.createTempFile("pants_target_specs", ".in");
    try (FileWriter targetSpecsFileWriter = new FileWriter(targetSpecsFile)) {
      for (String targetSpec : getTargetSpecs()) {
        targetSpecsFileWriter.write(targetSpec);
        targetSpecsFileWriter.write('\n');
      }
    }
    if (PantsUtil.isCompatibleProjectPantsVersion(getProjectPath(), "1.25.0")) {
      commandLine.addParameter("--spec-file=" + targetSpecsFile.getPath());
    }
    else {
      commandLine.addParameter("--target-spec-file=" + targetSpecsFile.getPath());
    }
    commandLine.addParameter("--no-quiet");

    if (PantsUtil.isCompatibleProjectPantsVersion(getProjectPath(), "1.24.0")) {
      commandLine.addParameter("--export-available-target-types");
    }

    if (getOptions().isImportSourceDepsAsJars()) {
      commandLine.addParameter("export-dep-as-jar");
      commandLine.addParameter("--sources");
    }
    else {
      commandLine.addParameter("export");
    }
    commandLine.addParameter("--output-file=" + outputFile.getPath());
    commandLine.addParameter("--formatted"); // json outputs in a compact format
    if (myResolveSourcesAndDocsForJars) {
      commandLine.addParameter("--export-libraries-sources");
      commandLine.addParameter("--export-libraries-javadocs");
    }
    return commandLine;
  }

  @NotNull
  private List<String> getTargetSpecs() {
    // If project is opened via pants cli, the targets are in specs.
    return Collections.unmodifiableList(getOptions().getSelectedTargetSpecs());
  }

  /**
   * @return if successfully canceled all running processes. false if failed and there were no processes to cancel.
   */
  public boolean cancelAllProcesses() {
    if (myProcesses.isEmpty()) {
      return false;
    }
    for (Process process : myProcesses) {
      process.destroy();
    }
    myProcesses.forEach(Process::destroy);
    return true;
  }

  public String getAbsolutePathFromWorkingDir(@NotNull String relativePath) {
    return new File(getBuildRoot(), relativePath).getPath();
  }

  private static class MyPantsCompileOptions implements PantsCompileOptions {

    private final String myExternalProjectPath;
    private final PantsExecutionOptions myExecutionOptions;

    private MyPantsCompileOptions(@NotNull String externalProjectPath, @NotNull PantsExecutionOptions executionOptions) {
      myExternalProjectPath = PantsUtil.resolveSymlinks(externalProjectPath);
      myExecutionOptions = executionOptions;
    }

    @NotNull
    @Override
    public String getExternalProjectPath() {
      return myExternalProjectPath;
    }

    @NotNull
    public List<String> getSelectedTargetSpecs() {
      return myExecutionOptions.getSelectedTargetSpecs();
    }

    public Optional<Integer> incrementalImportDepth() {
      return myExecutionOptions.incrementalImportDepth();
    }

    @Override
    public boolean isImportSourceDepsAsJars() {
      return myExecutionOptions.isImportSourceDepsAsJars();
    }
  }
}