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.lcms.db.io;
  
  import com.act.lcms.db.model.ChemicalAssociatedWithPathway;
  import com.act.lcms.db.model.LCMSWell;
  import com.act.lcms.db.model.Plate;
  import com.act.lcms.db.model.PlateWell;
  import com.act.lcms.db.model.ScanFile;
  import com.act.lcms.db.model.StandardWell;
  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.commons.lang3.tuple.Pair;
  
  import java.io.File;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  public class PrintConstructInfo {
    public static final String OPTION_DIRECTORY = "d";
    public static final String OPTION_CONSTRUCT = "c";
  
    public static final String HELP_MESSAGE = StringUtils.join(new String[]{
        "Prints information about a construct's composition and plate location."
    }, "");
    public static final HelpFormatter HELP_FORMATTER = new HelpFormatter();
  
    static {
      HELP_FORMATTER.setWidth(100);
    }
  
    public static final List<Option.Builder> OPTION_BUILDERS = new ArrayList<Option.Builder>() {{
      add(Option.builder(OPTION_DIRECTORY)
              .argName("directory")
              .desc("The directory where LCMS analysis results live")
              .hasArg().required()
              .longOpt("data-dir")
      );
      add(Option.builder(OPTION_CONSTRUCT)
              .argName("construct id")
              .desc("A construct whose data to search for")
              .hasArg().required()
              .longOpt("construct")
      );
  
      // Everybody needs a little help from their friends.
      add(Option.builder("h")
              .argName("help")
              .desc("Prints this help message")
              .longOpt("help")
      );
    }};
  
    static {
      // Add DB connection options.
      OPTION_BUILDERS.addAll(DB.DB_OPTION_BUILDERS);
    }
  
    public static void main(String[] args) throws Exception {
      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(LoadPlateCompositionIntoDB.class.getCanonicalName(), HELP_MESSAGE, opts, null, true);
        System.exit(1);
      }
  
      if (cl.hasOption("help")) {
        HELP_FORMATTER.printHelp(LoadPlateCompositionIntoDB.class.getCanonicalName(), HELP_MESSAGE, opts, null, true);
        return;
      }
  
      File lcmsDir = new File(cl.getOptionValue(OPTION_DIRECTORY));
      if (!lcmsDir.isDirectory()) {
        System.err.format("File at %s is not a directory\n", lcmsDir.getAbsolutePath());
        HELP_FORMATTER.printHelp(LoadPlateCompositionIntoDB.class.getCanonicalName(), HELP_MESSAGE, opts, null, true);
        System.exit(1);
      }
  
      try (DB db = DB.openDBFromCLI(cl)) {
        System.out.print("Loading/updating LCMS scan files into DB\n");
        ScanFile.insertOrUpdateScanFilesInDirectory(db, lcmsDir);
  
        String construct = cl.getOptionValue(OPTION_CONSTRUCT);
        List<LCMSWell> lcmsWells = LCMSWell.getInstance().getByConstructID(db, construct);
        Collections.sort(lcmsWells, new Comparator<LCMSWell>() {
          @Override
          public int compare(LCMSWell o1, LCMSWell o2) {
            return o1.getId().compareTo(o2.getId());
          }
        });
  
        Set<String> uniqueMSIDs = new HashSet<>();
        Map<Integer, Plate> platesById = new HashMap<>();
  
        System.out.format("\n\n-- Construct %s --\n\n", construct);
  
        List<ChemicalAssociatedWithPathway> pathwayChems =
            ChemicalAssociatedWithPathway.getInstance().getChemicalsAssociatedWithPathwayByConstructId(db, construct);
        System.out.print("Chemicals associated with pathway:\n");
        System.out.format("  %-8s%-15s%-45s\n", "index", "kind", "chemical");
        for (ChemicalAssociatedWithPathway chem : pathwayChems) {
          System.out.format("  %-8d%-15s%-45s\n", chem.getIndex(), chem.getKind(), chem.getChemical());
        }
  
        System.out.print("\nLCMS wells:\n");
        System.out.format("  %-15s%-6s%-15s%-15s%-15s\n", "barcode", "well", "msid", "fed", "lcms_count");
        for (LCMSWell well : lcmsWells) {
          uniqueMSIDs.add(well.getMsid());
  
          Plate p = platesById.get(well.getPlateId());
          if (p == null) {
            // TODO: migrate Plate to be a subclass of BaseDBModel.
            p = Plate.getPlateById(db, well.getPlateId());
            platesById.put(p.getId(), p);
          }
  
          String chem = well.getChemical();
          List<ScanFile> scanFiles = ScanFile.getScanFileByPlateIDRowAndColumn(
              db, p.getId(), well.getPlateRow(), well.getPlateColumn());
  
          System.out.format("  %-15s%-6s%-15s%-15s%-15d\n", p.getBarcode(),
              well.getCoordinatesString(), well.getMsid(),
              chem == null || chem.isEmpty() ? "--" : chem, scanFiles.size());
          System.out.flush();
        }
  
        List<Integer> plateIds = Arrays.asList(platesById.keySet().toArray(new Integer[platesById.size()]));
        Collections.sort(plateIds);
        System.out.print("\nAppears in plates:\n");
        for (Integer id : plateIds) {
          Plate p = platesById.get(id);
          System.out.format("  %s: %s\n", p.getBarcode(), p.getName());
        }
  
        List<String> msids = Arrays.asList(uniqueMSIDs.toArray(new String[uniqueMSIDs.size()]));
        Collections.sort(msids);
        System.out.format("\nMSIDS: %s\n", StringUtils.join(msids, ", "));
  
        Set<String> availableNegativeControls = new HashSet<>();
        for (Map.Entry<Integer, Plate> entry : platesById.entrySet()) {
          List<LCMSWell> wells = LCMSWell.getInstance().getByPlateId(db, entry.getKey());
          for (LCMSWell well : wells) {
            if (!construct.equals(well.getComposition())) {
              availableNegativeControls.add(well.getComposition());
            }
          }
        }
  
        // Print available standards for each step w/ plate barcodes and coordinates.
        System.out.format("\nAvailable Standards:\n");
        Map<Integer, Plate> plateCache = new HashMap<>();
        for (ChemicalAssociatedWithPathway chem : pathwayChems) {
          List<StandardWell> matchingWells =
              StandardWell.getInstance().getStandardWellsByChemical(db, chem.getChemical());
          for (StandardWell well : matchingWells){
            if (!plateCache.containsKey(well.getPlateId())) {
              Plate p = Plate.getPlateById(db, well.getPlateId());
              plateCache.put(p.getId(), p);
            }
          }
          Map<Integer, List<StandardWell>> standardWellsByPlateId = new HashMap<>();
          for (StandardWell well : matchingWells) {
            List<StandardWell> plateWells = standardWellsByPlateId.get(well.getPlateId());
            if (plateWells == null) {
              plateWells = new ArrayList<>();
              standardWellsByPlateId.put(well.getPlateId(), plateWells);
            }
            plateWells.add(well);
          }
          List<Pair<String, Integer>> plateBarcodes = new ArrayList<>(plateCache.size());
          for (Plate p : plateCache.values()) {
            if (p.getBarcode() == null) {
              plateBarcodes.add(Pair.of("(no barcode)", p.getId()));
            } else {
              plateBarcodes.add(Pair.of(p.getBarcode(), p.getId()));
            }
          }
          Collections.sort(plateBarcodes);
          System.out.format("  %s:\n", chem.getChemical());
          for (Pair<String, Integer> barcodePair : plateBarcodes) {
            // TODO: hoist this whole sorting/translation step into a utility class.
            List<StandardWell> wells = standardWellsByPlateId.get(barcodePair.getRight());
            if (wells == null) {
              // Don't print plates that don't apply to this chemical, which can happen because we're caching the plates.
              continue;
            }
            Collections.sort(wells, new Comparator<StandardWell>() {
              @Override
              public int compare(StandardWell o1, StandardWell o2) {
                int c = o1.getPlateRow().compareTo(o2.getPlateRow());
                if (c != 0) return c;
                return o1.getPlateColumn().compareTo(o2.getPlateColumn());
              }
            });
            List<String> descriptions = new ArrayList<>(wells.size());
            for (StandardWell well : wells) {
              descriptions.add(String.format("%s in %s%s", well.getCoordinatesString(), well.getMedia(),
                  well.getConcentration() == null ? "" : String.format(" c. %f", well.getConcentration())));
            }
            System.out.format("    %s: %s\n", barcodePair.getLeft(), StringUtils.join(descriptions, ", "));
          }
        }
  
        List<String> negativeControlStrains =
            Arrays.asList(availableNegativeControls.toArray(new String[availableNegativeControls.size()]));
        Collections.sort(negativeControlStrains);
        System.out.format("\nAvailable negative controls: %s\n", StringUtils.join(negativeControlStrains, ","));
        System.out.print("\n----------\n");
        System.out.print("\n\n");
      }
    }
  }
  