content: /*************************************************************************
  *                                                                        *
  *  This file is part of the 20n/act project.                             *
  *  20n/act enables DNA prediction for synthetic biology/bioengineering.  *
  *  Copyright (C) 2017 20n Labs, Inc.                                     *
  *                                                                        *
  *  Please direct all queries to act@20n.com.                             *
  *                                                                        *
  *  This program is free software: you can redistribute it and/or modify  *
  *  it under the terms of the GNU General Public License as published by  *
  *  the Free Software Foundation, either version 3 of the License, or     *
  *  (at your option) any later version.                                   *
  *                                                                        *
  *  This program is distributed in the hope that it will be useful,       *
  *  but WITHOUT ANY WARRANTY; without even the implied warranty of        *
  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
  *  GNU General Public License for more details.                          *
  *                                                                        *
  *  You should have received a copy of the GNU General Public License     *
  *  along with this program.  If not, see <http://www.gnu.org/licenses/>. *
  *                                                                        *
  *************************************************************************/
  
  package com.act.biointerpretation.mechanisminspection;
  
  import act.server.MongoDB;
  import act.server.NoSQLAPI;
  import act.shared.Chemical;
  import act.shared.Reaction;
  import chemaxon.calculations.clean.Cleaner;
  import chemaxon.formats.MolExporter;
  import chemaxon.formats.MolFormatException;
  import chemaxon.formats.MolImporter;
  import chemaxon.struc.Molecule;
  import chemaxon.struc.RxnMolecule;
  import org.apache.commons.cli.CommandLine;
  import org.apache.commons.cli.CommandLineParser;
  import org.apache.commons.cli.DefaultParser;
  import org.apache.commons.cli.HelpFormatter;
  import org.apache.commons.cli.Option;
  import org.apache.commons.cli.Options;
  import org.apache.commons.cli.ParseException;
  import org.apache.commons.lang3.StringUtils;
  import org.apache.logging.log4j.LogManager;
  import org.apache.logging.log4j.Logger;
  
  import java.io.File;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.List;
  
  public class ReactionRenderer {
  
    private static final Logger LOGGER = LogManager.getFormatterLogger(ReactionRenderer.class);
  
    public static final String OPTION_READ_DB = "d";
    public static final String OPTION_RXN_IDS = "r";
    public static final String OPTION_DIR_PATH = "f";
    public static final String OPTION_FILE_FORMAT = "e";
    public static final String OPTION_HEIGHT = "y";
    public static final String OPTION_WIDTH = "x";
    public static final String OPTION_COFACTOR = "c";
  
    public static final String HELP_MESSAGE = StringUtils.join(new String[] {
        "This class renders representations of a reaction."
    }, "");
  
    public static final List<Option.Builder> OPTION_BUILDERS = new ArrayList<Option.Builder>() {{
      add(Option.builder(OPTION_READ_DB)
          .argName("read db name")
          .desc("The name of the read DB to use")
          .hasArg().required()
          .longOpt("db")
      );
      add(Option.builder(OPTION_RXN_IDS)
          .argName("id")
          .desc("The id of the reaction to validate")
          .hasArgs()
          .valueSeparator(',')
          .required()
          .longOpt("id")
      );
      add(Option.builder(OPTION_DIR_PATH)
          .argName("dir path")
          .desc("The dir path where the image will be rendered")
          .hasArg().required()
          .longOpt("dir path")
      );
      // The list of file formats supported are here: https://marvin-demo.chemaxon.com/marvin/help/formats/formats.html
      add(Option.builder(OPTION_FILE_FORMAT)
          .argName("file format")
          .desc("The file format for the image")
          .hasArg().required()
          .longOpt("file format")
      );
      add(Option.builder(OPTION_HEIGHT)
          .argName("height")
          .desc("height of image")
          .hasArg()
          .longOpt("height")
      );
      add(Option.builder(OPTION_WIDTH)
          .argName("width")
          .desc("width of image")
          .hasArg()
          .longOpt("width")
      );
      add(Option.builder(OPTION_COFACTOR)
          .argName("cofactor")
          .desc("true if cofactors need to be rendered, false otherwise")
          .hasArg()
          .longOpt("cofactor")
      );
      add(Option.builder("h")
          .argName("help")
          .desc("Prints this help message")
          .longOpt("help")
      );
    }};
    public static final HelpFormatter HELP_FORMATTER = new HelpFormatter();
  
    static {
      HELP_FORMATTER.setWidth(100);
    }
  
    private static final String DEFAULT_FORMAT = "png";
    private static final Integer DEFAULT_WIDTH = 1000;
    private static final Integer DEFAULT_HEIGHT = 1000;
    private static final String XENON_INCHI = "InChI=1S/Xe";
    private static final String SMARTS_FORMAT = "smarts";
  
    String format;
    Integer width;
    Integer height;
  
    public ReactionRenderer() {
      this.format = DEFAULT_FORMAT;
      this.width = DEFAULT_WIDTH;
      this.height = DEFAULT_HEIGHT;
    }
  
    public ReactionRenderer(String format, Integer width, Integer height) {
      this.format = format;
      this.width = width;
      this.height = height;
    }
  
    public void drawReaction(MongoDB db, Long reactionId, String dirPath, boolean includeCofactors) throws IOException {
  
      RxnMolecule renderedReactionMolecule = getRxnMolecule(db, reactionId, includeCofactors);
  
      String fileName = StringUtils.join(new String[] {reactionId.toString(), ".", format});
      drawRxnMolecule(renderedReactionMolecule, new File(dirPath, fileName));
    }
  
    public String getSmartsForReaction(MongoDB db, Long reactionId, boolean includeCofactors) throws IOException {
      RxnMolecule rxnMolecule = getRxnMolecule(db, reactionId, includeCofactors);
      return getSmartsNotation(rxnMolecule);
    }
  
  
    public RxnMolecule getRxnMolecule(MongoDB db, Long reactionId, boolean includeCofactors) {
      RxnMolecule renderedReactionMolecule = new RxnMolecule();
  
      List<Long> substrateIds = getSubstrates(db, reactionId, includeCofactors);
      List<Long> productIds = getProducts(db, reactionId, includeCofactors);
  
      for (Long sub : substrateIds) {
        Chemical chemical = db.getChemicalFromChemicalUUID(sub);
        Molecule mol = importMoleculeOrXenon(chemical);
        renderedReactionMolecule.addComponent(mol, RxnMolecule.REACTANTS);
      }
  
      for (Long prod : productIds) {
        Chemical chemical = db.getChemicalFromChemicalUUID(prod);
        Molecule mol = importMoleculeOrXenon(chemical);
        renderedReactionMolecule.addComponent(mol, RxnMolecule.PRODUCTS);
      }
  
      return renderedReactionMolecule;
    }
  
    private List<Long> getSubstrates(MongoDB db, Long reactionId, boolean includeCofactors) {
      Reaction r = db.getReactionFromUUID(reactionId);
  
      List<Long> substrates = new ArrayList<>();
  
      for (Long id : r.getSubstrates()) {
        substrates.add(id);
      }
  
      if (includeCofactors) {
        for (Long id : r.getSubstrateCofactors()) {
          substrates.add(id);
        }
      }
  
      return substrates;
    }
  
    private List<Long> getProducts(MongoDB db, Long reactionId, boolean includeCofactors) {
      Reaction r = db.getReactionFromUUID(reactionId);
  
      List<Long> products = new ArrayList<>();
  
      for (Long id : r.getProducts()) {
        products.add(id);
      }
  
      if (includeCofactors) {
        for (Long id : r.getProductCofactors()) {
          products.add(id);
        }
      }
  
      return products;
    }
  
    public void drawRxnMolecule(RxnMolecule molecule, File imageFile) throws IOException {
      // Change the reaction arrow type.
      molecule.setReactionArrowType(RxnMolecule.REGULAR_SINGLE);
  
      drawMolecule(molecule, imageFile);
    }
  
    public void drawMolecule(Molecule molecule, File imageFile)
        throws IOException {
      // Calculate coordinates with a 2D coordinate system.
      Cleaner.clean(molecule, 2, null);
  
      byte[] graphics = MolExporter.exportToBinFormat(molecule, getFormatAndSizeString());
  
      try (FileOutputStream fos = new FileOutputStream(imageFile)) {
        fos.write(graphics);
      }
    }
  
    public String getSmartsNotation(Molecule mol) throws IOException {
      return MolExporter.exportToFormat(mol, SMARTS_FORMAT);
    }
  
    /**
     * Imports the molecule if possible, or else returns a Xenon atom as a placeholder for rendering.
     *
     * @throws MolFormatException
     */
    private Molecule importMoleculeOrXenon(Chemical chemical) {
      try {
        return chemical.importAsMolecule();
      } catch (MolFormatException e) {
        LOGGER.warn("No molecule returned for chemical %d. Replacing with Xenon.", chemical.getUuid());
        try {
          return MolImporter.importMol(XENON_INCHI);
        } catch (MolFormatException f) {
          LOGGER.error("Could not import xenon inchi; something is very wrong.");
          throw new RuntimeException(f);
        }
      }
    }
  
    public String getFormat() {
      return format;
    }
  
    public void setFormat(String format) {
      this.format = format;
    }
  
    public Integer getWidth() {
      return width;
    }
  
    public void setWidth(Integer width) {
      this.width = width;
    }
  
    public Integer getHeight() {
      return height;
    }
  
    public void setHeight(Integer height) {
      this.height = height;
    }
  
    private String getFormatAndSizeString() {
      return format + StringUtils.join(":w", width.toString(), ",", "h", height.toString());
    }
  
  
    public static void main(String[] args) throws IOException {
      Options opts = new Options();
      for (Option.Builder b : OPTION_BUILDERS) {
        opts.addOption(b.build());
      }
  
      CommandLine cl = null;
      try {
        CommandLineParser parser = new DefaultParser();
        cl = parser.parse(opts, args);
      } catch (ParseException e) {
        System.err.format("Argument parsing failed: %s\n", e.getMessage());
        HELP_FORMATTER.printHelp(ReactionRenderer.class.getCanonicalName(), HELP_MESSAGE, opts, null, true);
        System.exit(1);
      }
  
      if (cl.hasOption("help")) {
        HELP_FORMATTER.printHelp(ReactionRenderer.class.getCanonicalName(), HELP_MESSAGE, opts, null, true);
        return;
      }
  
      Integer height = Integer.parseInt(cl.getOptionValue(OPTION_HEIGHT, "1000"));
      Integer width = Integer.parseInt(cl.getOptionValue(OPTION_WIDTH, "1000"));
      Boolean representCofactors = cl.hasOption(OPTION_COFACTOR) && Boolean.parseBoolean(cl.getOptionValue(OPTION_COFACTOR));
  
      NoSQLAPI api = new NoSQLAPI(cl.getOptionValue(OPTION_READ_DB), cl.getOptionValue(OPTION_READ_DB));
  
      for (String val : cl.getOptionValues(OPTION_RXN_IDS)) {
        Long reactionId = Long.parseLong(val);
        ReactionRenderer renderer = new ReactionRenderer(cl.getOptionValue(OPTION_FILE_FORMAT), width, height);
        renderer.drawReaction(api.getReadDB(), reactionId, cl.getOptionValue(OPTION_DIR_PATH), representCofactors);
        LOGGER.info(renderer.getSmartsForReaction(api.getReadDB(), reactionId, representCofactors));
      }
    }
  }
  