/*
 *                  Eoulsan development code
 *
 * This code may be freely distributed and modified under the
 * terms of the GNU Lesser General Public License version 2.1 or
 * later and CeCILL-C. This should be distributed with the code.
 * If you do not have a copy, see:
 *
 *      http://www.gnu.org/licenses/lgpl-2.1.txt
 *      http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.txt
 *
 * Copyright for this code is held jointly by the Genomic platform
 * of the Institut de Biologie de l'École normale supérieure and
 * the individual authors. These should be listed in @author doc
 * comments.
 *
 * For more information on the Eoulsan project and its aims,
 * or to join the Eoulsan Google group, visit the home page
 * at:
 *
 *      http://outils.genomique.biologie.ens.fr/eoulsan
 *
 */

package fr.ens.biologie.genomique.eoulsan.core.workflow;

import static fr.ens.biologie.genomique.kenetre.util.StringUtils.xmlEscape;
import static java.util.Objects.requireNonNull;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import fr.ens.biologie.genomique.eoulsan.EoulsanLogger;
import fr.ens.biologie.genomique.eoulsan.Globals;
import fr.ens.biologie.genomique.eoulsan.core.Parameter;
import fr.ens.biologie.genomique.eoulsan.core.Step;
import fr.ens.biologie.genomique.eoulsan.data.DataFile;
import fr.ens.biologie.genomique.eoulsan.data.DataFiles;
import fr.ens.biologie.genomique.eoulsan.data.DataFormat;
import fr.ens.biologie.genomique.kenetre.util.StringUtils;
import fr.ens.biologie.genomique.kenetre.util.SystemUtils;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Convert a Workflow to Graphviz
 *
 * @author Laurent Jourdren
 * @since 2.0
 */
public class Workflow2Graphviz {

  private static final String GRAPHVIZ_COMMAND_NAME = "dot";

  private final AbstractWorkflow workflow;
  private final DataFile dotFile;
  private final DataFile imageFile;

  private void addRow(final StringBuilder sb, final String s) {

    sb.append("<tr><td bgcolor=\"white\" align=\"center\" colspan=\"2\"><font color=\"black\">");
    sb.append(s);
    sb.append("</font></td></tr>");
  }

  /**
   * Convert the workflow to Graphviz format
   *
   * @return a string with the workflow converted to Graphviz format
   */
  private String convert() {

    final StringBuilder sb = new StringBuilder();

    sb.append("## Generated by " + Globals.APP_NAME + " ");
    sb.append(Globals.APP_VERSION_STRING);
    sb.append('\n');
    sb.append("## Command to get the layout: \"" + GRAPHVIZ_COMMAND_NAME + " ");
    sb.append(StringUtils.join(createCommandLine(true), " "));
    sb.append("\"\n");

    sb.append(
        "digraph g {\n  graph [fontsize=30 labelloc=\"t\" label=\"\" "
            + "splines=true overlap=false rankdir = \"LR\"]\n"
            + "  ratio = auto;\n");

    // Create nodes
    for (Step step : this.workflow.getSteps()) {

      if (step == this.workflow.getFirstStep() || step == this.workflow.getCheckerStep()) {
        continue;
      }

      sb.append("  \"step");
      sb.append(step.getNumber());
      sb.append(
          "\" [ style = \"filled\" penwidth = 1 fillcolor = \"white\" fontname = \"Courier New\" shape = \"Mrecord\" label =");

      sb.append("<<table border=\"0\" cellborder=\"0\" cellpadding=\"3\" bgcolor=\"white\">");

      sb.append("<tr><td bgcolor=\"black\" align=\"center\" colspan=\"2\"><font color=\"white\">");
      sb.append(step.getId());
      sb.append("</font></td></tr>");

      addRow(sb, step.getModuleName() + " " + step.getStepVersion());
      for (Parameter p : step.getParameters()) {
        addRow(sb, xmlEscape(p.getName()) + " = " + xmlEscape(p.getValue()));
      }

      sb.append("</table>> ] ;\n");
    }

    sb.append('\n');

    final Multimap<AbstractStep, AbstractStep> linkedSteps = HashMultimap.create();

    // Create links from output ports
    for (Step step : this.workflow.getSteps()) {

      // Do not handle first and check step
      if (step == this.workflow.getFirstStep() || step == this.workflow.getCheckerStep()) {
        continue;
      }

      final int stepNumber = step.getNumber();
      AbstractStep abstractStep = (AbstractStep) step;

      // For each port
      for (StepOutputPort outputPort : abstractStep.getWorkflowOutputPorts()) {

        // For each port link
        for (StepInputPort link : outputPort.getLinks()) {

          final AbstractStep linkedStep = link.getStep();

          // Do not handle first and check step
          if (linkedStep == this.workflow.getFirstStep()
              || linkedStep == this.workflow.getCheckerStep()) {
            continue;
          }

          linkedSteps.put(linkedStep, abstractStep);

          sb.append("  step");
          sb.append(stepNumber);
          sb.append(" -> step");
          sb.append(linkedStep.getNumber());
          sb.append(" [ penwidth = 5 fontsize = 28 fontcolor = \"black\" label = \"");

          final DataFormat format = outputPort.getFormat();

          String formatName =
              format.getAlias() == null || "".equals(format.getAlias())
                  ? format.getName()
                  : format.getAlias();

          sb.append(formatName);
          sb.append("\" ];\n");
        }
      }
    }

    // Create other links between steps
    for (Step step : this.workflow.getSteps()) {

      // Do not handle first and check step
      if (step == this.workflow.getFirstStep() || step == this.workflow.getCheckerStep()) {
        continue;
      }

      AbstractStep abstractStep = (AbstractStep) step;

      StepStateDependencies observer = ((AbstractStep) step).getStepStateDependencies();
      Set<AbstractStep> requiredSteps = new HashSet<>(observer.getRequiredSteps());

      requiredSteps.removeAll(linkedSteps.get(abstractStep));

      // Do not handle first and check step
      requiredSteps.remove(this.workflow.getFirstStep());
      requiredSteps.remove(this.workflow.getCheckerStep());

      for (AbstractStep requiredStep : requiredSteps) {

        sb.append("  step");
        sb.append(requiredStep.getNumber());
        sb.append(" -> step");
        sb.append(step.getNumber());
        sb.append(" [ penwidth = 5 fontsize = 28 fontcolor = \"green\"");
        sb.append(" ];\n");
      }
    }

    sb.append("}\n");

    return sb.toString();
  }

  /**
   * Convert and save the workflow as a Graphviz file.
   *
   * @throws IOException if an error occurs while creating the output file
   */
  public void saveDotFile() throws IOException {
    saveDotFile(this.dotFile);
  }

  /**
   * Convert and save the workflow as a Graphviz file.
   *
   * @throws IOException if an error occurs while creating the output file
   */
  private void saveDotFile(final DataFile outputFile) throws IOException {

    final Writer writer = new OutputStreamWriter(outputFile.create(), Charset.defaultCharset());
    writer.write(convert());
    writer.close();
  }

  /**
   * Convert and save the workflow as an image file.
   *
   * @return true if image successfully saved
   */
  public boolean saveImageFile() {

    final DataFile tmpDotDataFile;
    final DataFile tmpImageDataFile;
    final File dotExecutableFile;

    try {

      File tmpDotFile = File.createTempFile("eoulsan-workflow-", ".dot");
      File tmpImageFile = File.createTempFile("eoulsan-workflow-", ".png");

      tmpDotDataFile = new DataFile(tmpDotFile);
      tmpImageDataFile = new DataFile(tmpImageFile);

      // Save temporary dot file
      saveDotFile(tmpDotDataFile);

      // Copy workflow dot file
      DataFiles.copy(tmpDotDataFile, this.dotFile);

      // Get the dot executable path
      dotExecutableFile = SystemUtils.searchExecutableInPATH("dot");
      if (dotExecutableFile == null) {

        EoulsanLogger.logWarning(
            "Unable to find the \"dot\" command in the PATH"
                + " to create the workflow image file");

        // Delete temporary files
        tmpDotDataFile.delete();
        tmpImageDataFile.delete();

        return false;
      }
    } catch (IOException e) {
      EoulsanLogger.logWarning("Unable to create workflow image: " + e.getMessage());
      return false;
    }

    // Create command line
    List<String> command = new ArrayList<>();
    command.add(dotExecutableFile.getAbsolutePath());
    command.addAll(createCommandLine(tmpDotDataFile, tmpImageDataFile, true));

    ProcessBuilder pb = new ProcessBuilder(command);
    try {
      Process p = pb.start();
      int exitcode = p.waitFor();

      if (exitcode != 0) {
        EoulsanLogger.logWarning("Unable to create workflow image: dot exit error: " + exitcode);
        return false;
      }

      // Copy workflow image
      DataFiles.copy(tmpImageDataFile, this.imageFile);

      return true;
    } catch (IOException | InterruptedException e) {
      EoulsanLogger.logWarning("Unable to create workflow image: " + e.getMessage());
      return false;
    } finally {

      // Delete temporary files
      tmpDotDataFile.toFile().delete();
      tmpImageDataFile.toFile().delete();
    }
  }

  /**
   * Create GraphViz command line.
   *
   * @param absolustePath true if the absolute path must be used in the command line
   * @return a list with the command line
   */
  private List<String> createCommandLine(final boolean absolustePath) {

    return createCommandLine(this.dotFile, this.imageFile, absolustePath);
  }

  /**
   * Create GraphViz command line.
   *
   * @param absolustePath true if the absolute path must be used in the command line
   * @param dotFile output dot file
   * @param imageFile output image file
   * @return a list with the command line
   */
  private static List<String> createCommandLine(
      final DataFile dotFile, final DataFile imageFile, final boolean absolustePath) {

    return Arrays.asList(
        "-Gsize=40",
        "-Tpng",
        filePath(dotFile, absolustePath),
        "-o",
        filePath(imageFile, absolustePath));
  }

  private static String filePath(final DataFile file, final boolean absolustePath) {

    return absolustePath ? file.getSource() : file.getName();
  }

  //
  // Constructor
  //

  /**
   * Public constructor.
   *
   * @param workflow the workflow
   * @param dotFile output dot file
   * @param imageFile output image file
   */
  public Workflow2Graphviz(
      final AbstractWorkflow workflow, final DataFile dotFile, final DataFile imageFile) {

    requireNonNull(workflow, "workflow parameter cannot be null");
    requireNonNull(dotFile, "dotFile parameter cannot be null");
    requireNonNull(imageFile, "imageFile parameter cannot be null");

    this.workflow = workflow;
    this.dotFile = dotFile;
    this.imageFile = imageFile;
  }
}
