package fr.ens.biologie.genomique.eoulsan.modules.chipseq.peakcalling;

import static fr.ens.biologie.genomique.eoulsan.EoulsanLogger.getLogger;
import static fr.ens.biologie.genomique.eoulsan.modules.chipseq.ChIPSeqDataFormats.PEAK;

import com.google.common.base.Joiner;
import fr.ens.biologie.genomique.eoulsan.EoulsanException;
import fr.ens.biologie.genomique.eoulsan.Globals;
import fr.ens.biologie.genomique.eoulsan.annotations.LocalOnly;
import fr.ens.biologie.genomique.eoulsan.core.InputPorts;
import fr.ens.biologie.genomique.eoulsan.core.InputPortsBuilder;
import fr.ens.biologie.genomique.eoulsan.core.Modules;
import fr.ens.biologie.genomique.eoulsan.core.OutputPorts;
import fr.ens.biologie.genomique.eoulsan.core.OutputPortsBuilder;
import fr.ens.biologie.genomique.eoulsan.core.Parameter;
import fr.ens.biologie.genomique.eoulsan.core.StepConfigurationContext;
import fr.ens.biologie.genomique.eoulsan.core.TaskContext;
import fr.ens.biologie.genomique.eoulsan.core.TaskResult;
import fr.ens.biologie.genomique.eoulsan.core.TaskStatus;
import fr.ens.biologie.genomique.eoulsan.data.Data;
import fr.ens.biologie.genomique.eoulsan.data.DataMetadata;
import fr.ens.biologie.genomique.eoulsan.design.Design;
import fr.ens.biologie.genomique.eoulsan.design.DesignUtils;
import fr.ens.biologie.genomique.eoulsan.design.Experiment;
import fr.ens.biologie.genomique.eoulsan.design.ExperimentSample;
import fr.ens.biologie.genomique.eoulsan.modules.AbstractModule;
import fr.ens.biologie.genomique.eoulsan.requirements.DockerRequirement;
import fr.ens.biologie.genomique.eoulsan.requirements.Requirement;
import fr.ens.biologie.genomique.eoulsan.util.EoulsanDockerManager;
import fr.ens.biologie.genomique.eoulsan.util.ProcessUtils;
import fr.ens.biologie.genomique.kenetre.util.Version;
import fr.ens.biologie.genomique.kenetre.util.process.SimpleProcess;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * This class uses tools from the BEDTools suite.
 *
 * @author Celine Hernandez - CSB lab - ENS - Paris
 */
@LocalOnly
public class BedToolsModule extends AbstractModule {

  /** Module name */
  private static final String MODULE_NAME = "bedtools";

  private static final String BEDTOOLS_DEFAULT_DOCKER_IMAGE = "genomicpariscentre/bedtools:latest";

  private Requirement requirement;
  private String dockerImage = BEDTOOLS_DEFAULT_DOCKER_IMAGE;

  /** Methods */
  @Override
  public String getName() {
    return MODULE_NAME;
  }

  @Override
  public String getDescription() {
    return "This step runs a tool from the BEDTools suite.";
  }

  @Override
  public Version getVersion() {
    return Globals.APP_VERSION;
  }

  @Override
  public InputPorts getInputPorts() {
    final InputPortsBuilder builder = new InputPortsBuilder();
    builder.addPort("inputlist", true, PEAK);
    return builder.create();
  }

  @Override
  public OutputPorts getOutputPorts() {
    final OutputPortsBuilder builder = new OutputPortsBuilder();
    builder.addPort("outputlist", true, PEAK);
    return builder.create();
  }

  /**
   * Set the parameters of the step to configure the step.
   *
   * @param stepParameters parameters of the step
   * @throws EoulsanException if a parameter is invalid
   */
  @Override
  public void configure(final StepConfigurationContext context, final Set<Parameter> stepParameters)
      throws EoulsanException {

    for (Parameter p : stepParameters) {

      switch (p.getName()) {
        case "docker.image":
          this.dockerImage = p.getStringValue().trim();
          if (this.dockerImage.isEmpty()) {
            Modules.badParameterValue(context, p, "The docker image name is empty");
          }
          break;

        default:
          Modules.unknownParameter(context, p);
          break;
      }
    }

    this.requirement = DockerRequirement.newDockerRequirement(this.dockerImage, true);
  }

  @Override
  public Set<Requirement> getRequirements() {
    return Collections.singleton(this.requirement);
  }

  /** Run bedtools multiinter. Installation (if needed) was made during configuration. */
  @Override
  public TaskResult execute(final TaskContext context, final TaskStatus status) {

    // Get input data (PEAK format, as generated by )
    final Data inData = context.getInputData(PEAK);

    final Design design = context.getWorkflow().getDesign();

    int count = 1;

    // Construction of a HashMap containing a SampleName as String corresponding
    // to a specific Data.
    Map<String, Data> nameMap = new HashMap<>();
    for (Data anInputData : inData.getListElements()) {
      String name = anInputData.getMetadata().getSampleName();
      nameMap.put(name, anInputData);
      getLogger().info(count + " Sample name: " + name);
      count = count + 1;
    }

    // First sort data into experiments/replicate groups before we can
    // concatenate what is inside each group
    Map<String, List<Data>> expMap = new HashMap<>();
    for (Experiment e : design.getExperiments()) {

      for (ExperimentSample expSam : e.getExperimentSamples()) {

        getLogger()
            .info(
                "Input file. ref : "
                    + DesignUtils.getReference(expSam)
                    + "| exp : "
                    + e.getName()
                    + "| rep : "
                    + DesignUtils.getRepTechGroup(expSam));

        boolean isReference =
            DesignUtils.getReference(expSam).toLowerCase(Globals.DEFAULT_LOCALE).equals("true");

        // if we have a control

        // Should we add its peaks also ? Not, for now.
        if (isReference) {
          getLogger().info("Reference file, not treated.");
          continue;
        }
        getLogger().info("Not a reference file. Proceeding.");

        // if we have a sample

        // Access the sample metadata and concatenate values
        // in order to create a key used to sort samples by category
        // (experiment and replicate group)
        String sortingKey = e.getName();

        // Store current sample in expmap
        // If it's the first in its category, create a new container before
        if (expMap.get(sortingKey) == null) {
          List<Data> tmpList = new ArrayList<>();
          tmpList.add(nameMap.get(expSam.getSample().getName()));
          expMap.put(sortingKey, tmpList);
        } else {
          expMap.get(sortingKey).add(nameMap.get(expSam.getSample().getName()));
        }
        getLogger()
            .info("Now " + expMap.get(sortingKey).size() + " samples for experiment " + sortingKey);
      }
    }

    final Data peakDataList = context.getOutputData(PEAK, "mergedPeaklist");

    // Loop through each sorted category
    // to apply bedtools mergeinter
    for (String experimentName : expMap.keySet()) {

      // Get all samples of current category
      List<Data> expDataList = expMap.get(experimentName);
      List<Data> expDataList2 = new ArrayList<>();

      for (int i = 0; i < expDataList.size(); i++) {
        getLogger().info("for " + experimentName + " Data " + i + " : " + expDataList.get(i));

        if (expDataList.get(i) != null) {
          expDataList2.add(expDataList.get(i));
        }
      }

      for (int j = 0; j < expDataList2.size(); j++) {
        getLogger()
            .info(
                "expDataList2; for " + experimentName + " Data " + j + " : " + expDataList2.get(j));
      }

      // Do not merge if we have only one file
      // Copy and change name to make it available in Eoulsan for the rest of
      // the analysis

      getLogger().info("File count for " + experimentName + " : " + expDataList2.size());

      if (expDataList.size() < 2) {
        getLogger().info("Data list contains only 1 sample for experiment " + experimentName);

        Data anInputData = expDataList2.get(0);

        final Data outputData =
            peakDataList.addDataToList(
                anInputData.getMetadata().getSampleName().replaceAll("[^a-zA-Z0-9]", ""));
        outputData.getMetadata().set(anInputData.getMetadata());

        try {
          anInputData.getDataFile().symlink(outputData.getDataFile());
        } catch (IOException ioe) {
          getLogger()
              .severe(
                  "Could not create symlink from "
                      + anInputData.getDataFile()
                      + " to "
                      + outputData.getDataFile());
          return status.createTaskResult();
        }

        continue;
      }

      // Create command line

      // Executable
      List<String> cmd = new ArrayList<>();

      cmd.add("bedtools");
      cmd.add("multiinter");

      // Add all files to be merged
      cmd.add("-i");
      for (Data sample : expDataList2) {
        cmd.add(String.format("%s", sample.getDataFilename()));
      }

      // Get metadata of one peak file
      DataMetadata metadata = expDataList2.get(0).getMetadata();

      getLogger().info("Test metadata " + metadata.getSampleName());

      // Define the merged file : all peaks
      final Data peakData =
          peakDataList.addDataToList(
              metadata.getSampleName().replaceAll("[^a-zA-Z0-9]", "") + "Mergedpeaks");
      peakData.getMetadata().set(metadata);

      // As bedtools writes directly its results on the standard output, this
      // outputFile will contain the merged peaks.
      // The outputFile will contain the standard output of the process.
      File outputFile = peakData.getDataFile().toFile();

      final File stderrFile = Path.of("docker.err").toFile();

      String cmd2 = Joiner.on(' ').join(cmd);
      getLogger().info("Run command line : " + cmd2);

      // Run bedtools
      try {

        final SimpleProcess process =
            EoulsanDockerManager.getInstance().createImageInstance(this.dockerImage);
        final int exitValue =
            process.execute(
                cmd,
                context.getStepOutputDirectory().toFile(),
                context.getLocalTempDirectory(),
                outputFile,
                stderrFile);

        ProcessUtils.throwExitCodeException(exitValue, Joiner.on(' ').join(cmd));
      } catch (IOException err) {
        return status.createTaskResult(err);
      }
    }
    return status.createTaskResult();
  }
}
