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.analysis;
  
  import com.act.lcms.XZ;
  import com.act.lcms.db.io.DB;
  import com.act.lcms.db.io.LoadPlateCompositionIntoDB;
  import com.act.lcms.db.model.ChemicalAssociatedWithPathway;
  import com.act.lcms.db.model.ConstructEntry;
  import com.act.lcms.db.model.Plate;
  import com.act.lcms.db.model.ScanFile;
  import com.act.lcms.db.model.StandardIonResult;
  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.csv.CSVFormat;
  import org.apache.commons.csv.CSVPrinter;
  import org.apache.commons.lang3.StringUtils;
  import org.apache.commons.lang3.tuple.Pair;
  
  import java.io.FileWriter;
  import java.io.IOException;
  import java.io.File;
  import java.sql.SQLException;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashSet;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.HashMap;
  
  public class StandardIonAnalysis {
    private static final boolean USE_SNR_FOR_LCMS_ANALYSIS = true;
    public static final String CSV_FORMAT = "csv";
    public static final String OPTION_DIRECTORY = "d";
    public static final String OPTION_CONSTRUCT = "c";
    public static final String OPTION_STANDARD_PLATE_BARCODE = "sp";
    public static final String OPTION_STANDARD_CHEMICAL = "sc";
    public static final String OPTION_OUTPUT_PREFIX = "o";
    public static final String OPTION_MEDIUM = "m";
    public static final String OPTION_PLOTTING_DIR = "p";
    public static final String OPTION_OVERRIDE_NO_SCAN_FILE_FOUND = "s";
  
    public static final String HELP_MESSAGE = StringUtils.join(new String[]{
        "TODO: write a help message."
    }, "");
    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")
          .desc("The construct whose pathway chemicals should be analyzed")
          .hasArg()
          .longOpt("construct")
      );
      add(Option.builder(OPTION_STANDARD_PLATE_BARCODE)
          .argName("standard plate barcode")
          .desc("The plate barcode to use when searching for a compatible standard")
          .hasArg()
          .longOpt("standard-plate")
      );
      add(Option.builder(OPTION_STANDARD_CHEMICAL)
          .argName("standard chemical")
          .desc("The standard chemical to analyze")
          .hasArg()
          .longOpt("standard-chemical")
      );
      add(Option.builder(OPTION_OUTPUT_PREFIX)
          .argName("output prefix")
          .desc("A prefix for the output data/pdf files")
          .hasArg().required()
          .longOpt("output-prefix")
      );
      add(Option.builder(OPTION_MEDIUM)
          .argName("medium")
          .desc("A name of the medium to search wells by.")
          .hasArg()
          .longOpt("medium")
      );
      add(Option.builder(OPTION_PLOTTING_DIR)
          .argName("plotting directory")
          .desc("The absolute path of the plotting directory")
          .hasArg().required()
          .longOpt("plotting-dir")
      );
      add(Option.builder(OPTION_OVERRIDE_NO_SCAN_FILE_FOUND)
          .argName("override option")
          .desc("Do not fail when the scan file cannot be found")
          .longOpt("override-option")
      );
    }};
    static {
      // Add DB connection options.
      OPTION_BUILDERS.addAll(DB.DB_OPTION_BUILDERS);
    }
  
    /**
     * This function gets all the best time windows from spectra in water and meoh media, so that they can analyzed
     * by the yeast media samples for snr analysis.
     * @param waterAndMeohSpectra A list of ions to best XZ value.
     * @return A map of ion to list of restricted time windows.
     */
    public static Map<String, List<Double>> getRestrictedTimeWindowsForIonsFromWaterAndMeOHMedia(
        List<LinkedHashMap<String, XZ>> waterAndMeohSpectra) {
  
      Map<String, List<Double>> ionToRestrictedTimeWindows = new HashMap<>();
  
      for (LinkedHashMap<String, XZ> entry : waterAndMeohSpectra) {
        for (String ion : entry.keySet()) {
          List<Double> restrictedTimes = ionToRestrictedTimeWindows.get(ion);
          if (restrictedTimes == null) {
            restrictedTimes = new ArrayList<>();
            ionToRestrictedTimeWindows.put(ion, restrictedTimes);
          }
          Double timeValue = entry.get(ion).getTime();
          restrictedTimes.add(timeValue);
        }
      }
  
      return ionToRestrictedTimeWindows;
    }
  
    /**
     * Given a construct id (like "pa1"), return the associated ConstructEntry object and a list of the chemical
     * products/byproducts associated with that pathway (including all intermediate and side-reaction products).
     * @param db The DB connection to query.
     * @param constructId The identifier for the constructs whose products should be queried (like "pa1").
     * @return A pair of the ConstructEntry for the specified construct id and a list of chemical products associated
     *         with that pathway.
     * @throws SQLException
     */
    public Pair<ConstructEntry, List<ChemicalAssociatedWithPathway>> getChemicalsForConstruct(DB db, String constructId)
        throws SQLException {
      ConstructEntry construct =
          ConstructEntry.getInstance().getCompositionMapEntryByCompositionId(db, constructId);
      if (construct == null) {
        throw new RuntimeException(String.format("Unable to find construct '%s'", constructId));
      }
  
      List<ChemicalAssociatedWithPathway> products =
          ChemicalAssociatedWithPathway.getInstance().getChemicalsAssociatedWithPathwayByConstructId(db, constructId);
  
      return Pair.of(construct, products);
    }
  
    /**
     * Find all standard wells containing a specified chemical.
     * @param db The DB connection to query.
     * @param pathwayChem The chemical for which to find standard wells.
     * @return A list of standard wells (in any plate) containing the specified chemical.
     * @throws SQLException
     */
    public static List<StandardWell> getStandardWellsForChemical(DB db, String pathwayChem)
        throws SQLException {
      return StandardWell.getInstance().getStandardWellsByChemical(db, pathwayChem);
    }
  
    /**
     * Find all standard wells containing a specified chemical.
     * @param db The DB connection to query.
     * @param chemical The chemical for which to find standard wells.
     * @param plateId The plateId to filter by.
     * @return A list of standard wells (in any plate) containing the specified chemical.
     * @throws SQLException
     */
    public List<StandardWell> getStandardWellsForChemicalInSpecificPlate(DB db,
                                                                         String chemical,
                                                                         Integer plateId) throws SQLException {
      return StandardWell.getInstance().getStandardWellsByChemicalAndPlateId(db, chemical, plateId);
    }
  
    /**
     * Find all standard wells containing a specified chemical.
     * @param db The DB connection to query.
     * @param chemical The chemical for which to find standard wells.
     * @param plateId The plateId to filter by.
     * @param medium The medium of the plate to filter by.
     * @return A list of standard wells (in any plate) containing the specified chemical.
     * @throws SQLException
     */
    public List<StandardWell> getStandardWellsForChemicalInSpecificPlateAndMedium(DB db,
                                                                                  String chemical,
                                                                                  Integer plateId,
                                                                                  String medium) throws SQLException {
      return StandardWell.getInstance().getStandardWellsByChemicalAndPlateIdAndMedium(db, chemical, plateId, medium);
    }
  
    public static List<StandardWell> getViableNegativeControlsForStandardWell(DB db, StandardWell baseStandard)
        throws SQLException, IOException, ClassNotFoundException {
      List<StandardWell> wellsFromSamePlate = StandardWell.getInstance().getByPlateId(db, baseStandard.getPlateId());
  
      // TODO: take availability of scan files into account here?
      List<StandardWell> candidates = new ArrayList<>();
      for (StandardWell well : wellsFromSamePlate) {
        if (well.getChemical().equals(baseStandard.getChemical())) {
          continue; // Skip wells with the same chemical.
        }
  
        if (baseStandard.getConcentration() != null && well.getConcentration() != null &&
            !baseStandard.getConcentration().equals(well.getConcentration())) {
          continue; // Skip non-matching concentrations if both wells define concentration.
        }
  
        if (baseStandard.getMedia() != null && well.getMedia() != null &&
            !baseStandard.getMedia().equals(well.getMedia())) {
          continue; // Skip non-matching media if both wells define media type.
        }
        candidates.add(well);
      }
  
      return candidates;
    }
  
    /**
     * Given a standard well and viable negative control candidates, returns a map of mapping of all specified standard
     * wells to scan files sharing the ion modes available for the specified standard well.  For example, if the specified
     * standard well has only positive ion mode scan files available, the map will contain only positive ion mode scan
     * files for that well and all specified negativeCandidate wells.  If both positive and negative ion mode scan files
     * are available for the specified well, then, both positive and negative mode scan files will be included in the map.
     * @param db The DB connection to query.
     * @param primaryStandard The primary standard well being analysed.
     * @param negativeCandidates A list of standard wells that could be used as negative controls in the analysis.
     * @return A map from all specified standard wells (primary and negative controls) to a list of scan files.
     * @throws SQLException
     */
    public Map<StandardWell, List<ScanFile>> getViableScanFilesForStandardWells(
        DB db, StandardWell primaryStandard, List<StandardWell> negativeCandidates) throws SQLException {
      Map<StandardWell, List<ScanFile>> wellToFilesMap = new HashMap<>();
      List<ScanFile> posScanFiles = ScanFile.getScanFileByPlateIDRowAndColumn(
          db, primaryStandard.getPlateId(), primaryStandard.getPlateRow(), primaryStandard.getPlateColumn());
      wellToFilesMap.put(primaryStandard, posScanFiles);
  
      Set<ScanFile.SCAN_MODE> viableScanModes = new HashSet<>();
      for (ScanFile file : posScanFiles) {
        viableScanModes.add(file.getMode());
      }
  
      for (StandardWell well : negativeCandidates) {
        List<ScanFile> allScanFiles = ScanFile.getScanFileByPlateIDRowAndColumn(
            db, well.getPlateId(), well.getPlateRow(), well.getPlateColumn());
        List<ScanFile> viableScanFiles = new ArrayList<>();
        for (ScanFile file : allScanFiles) {
          if (viableScanModes.contains(file.getMode())) {
            viableScanFiles.add(file);
          }
        }
        wellToFilesMap.put(well, viableScanFiles);
      }
  
      return wellToFilesMap;
    }
  
    /**
     * This function returns the best SNR values and their times for each metlin ion based on the StandardIonResult
     * datastructure and plots diagnostics.
     * @param lcmsDir The directory where the LCMS scan data can be found.
     * @param db The DB connection to query.
     * @param positiveStandardWell This is the positive standard well against which the snr comparison is done.
     * @param negativeStandardWells These are the negative standard wells which are used for benchmarking.
     * @param plateCache A hash of Plates already accessed from the DB.
     * @param chemical This is chemical of interest we are running ion analysis against.
     * @param plottingDir This is the directory where the plotting diagnostics will live.
     * @param restrictedTimeWindows This map of ion to list of doubles represent time windows over which the analysis has
     *                              to be done.
     * @return The StandardIonResult datastructure which contains the standard ion analysis results.
     * @throws Exception
     */
    public static StandardIonResult getSnrResultsForStandardWellComparedToValidNegativesAndPlotDiagnostics(
        File lcmsDir, DB db, StandardWell positiveStandardWell, List<StandardWell> negativeStandardWells,
        HashMap<Integer, Plate> plateCache, String chemical, String plottingDir, Map<String, List<Double>> restrictedTimeWindows)
        throws Exception {
      Plate plate = plateCache.get(positiveStandardWell.getPlateId());
  
      if (plate == null) {
        plate = Plate.getPlateById(db, positiveStandardWell.getPlateId());
        plateCache.put(plate.getId(), plate);
      }
  
      List<Pair<String, Double>> searchMZs;
      Pair<String, Double> searchMZ = Utils.extractMassFromString(db, chemical);
      if (searchMZ != null) {
        searchMZs = Collections.singletonList(searchMZ);
      } else {
        throw new RuntimeException("Could not find Mass Charge value for " + chemical);
      }
  
      List<StandardWell> allWells = new ArrayList<>();
      allWells.add(positiveStandardWell);
      allWells.addAll(negativeStandardWells);
  
      ChemicalToMapOfMetlinIonsToIntensityTimeValues peakData = AnalysisHelper.readStandardWellScanData(
          db, lcmsDir, searchMZs, ScanData.KIND.STANDARD, plateCache, allWells, false, null, null,
          USE_SNR_FOR_LCMS_ANALYSIS, positiveStandardWell.getChemical());
  
      if (peakData == null || peakData.getIonList().size() == 0) {
        return null;
      }
  
      LinkedHashMap<String, XZ> snrResults =
          WaveformAnalysis.performSNRAnalysisAndReturnMetlinIonsRankOrderedBySNR(peakData, chemical, restrictedTimeWindows);
  
      String bestMetlinIon = AnalysisHelper.getBestMetlinIonFromPossibleMappings(snrResults);
  
      Map<String, String> plottingFileMappings =
          peakData.plotPositiveAndNegativeControlsForEachMetlinIon(searchMZ, plottingDir, chemical, allWells);
  
      StandardIonResult result = new StandardIonResult();
      result.setChemical(chemical);
      result.setAnalysisResults(snrResults);
      result.setStandardWellId(positiveStandardWell.getId());
      result.setPlottingResultFilePaths(plottingFileMappings);
      result.setBestMetlinIon(bestMetlinIon);
      return result;
    }
  
    /**
     * This function returns the best metlion ions of the SNR analysis for each well.
     * @param chemical - This is chemical of interest we are running ion analysis against.
     * @param lcmsDir - The directory where the LCMS scan data can be found.
     * @param db
     * @param standardWells - The standard wells over which the analysis is done. This list is sorted in the following
     *                      order: wells in water or meoh are processed first, followed by everything else. This ordering
     *                      is required since the analysis on yeast media depends on the analysis of results in water/meoh.
     * @param plottingDir - This is the directory where the plotting diagnostics will live.
     * @return A mapping of the well that was analyzed to the standard ion result.
     * @throws Exception
     */
    public static Map<StandardWell, StandardIonResult> getBestMetlinIonsForChemical(
        String chemical, File lcmsDir, DB db, List<StandardWell> standardWells, String plottingDir) throws Exception {
  
      Map<StandardWell, StandardIonResult> result = new HashMap<>();
      List<LinkedHashMap<String, XZ>> waterAndMeohSpectra = new ArrayList<>();
  
      for (StandardWell wellToAnalyze : standardWells) {
        List<StandardWell> negativeControls =
            StandardIonAnalysis.getViableNegativeControlsForStandardWell(db, wellToAnalyze);
  
        StandardIonResult cachingResult;
        if (StandardWell.doesMediaContainYeastExtract(wellToAnalyze.getMedia())) {
          // Since the standard wells are sorted in a way that the Water and MeOH media wells are analyzed first, we are
          // guaranteed to get the restricted time windows from these wells for the yeast media analysis.
          // TODO: Find a better way of doing this. There is a dependency on other ion analysis from other media and the way to
          // achieve this is by caching those ion runs first before the yeast media analysis. However, this algorithm is
          // dependant on which sequence gets analyzed first, which is brittle.
          Map<String, List<Double>> restrictedTimeWindows =
              getRestrictedTimeWindowsForIonsFromWaterAndMeOHMedia(waterAndMeohSpectra);
  
          cachingResult = StandardIonResult.getForChemicalAndStandardWellAndNegativeWells(
              lcmsDir, db, chemical, wellToAnalyze, negativeControls, plottingDir, restrictedTimeWindows);
        } else if (StandardWell.isMediaMeOH(wellToAnalyze.getMedia()) || StandardWell.isMediaWater(wellToAnalyze.getMedia())) {
          // If the media is not yeast, there is no time window restrictions, hence the last argument is null.
          cachingResult = StandardIonResult.getForChemicalAndStandardWellAndNegativeWells(
              lcmsDir, db, chemical, wellToAnalyze, negativeControls, plottingDir, null);
  
          if (cachingResult != null) {
            waterAndMeohSpectra.add(cachingResult.getAnalysisResults());
          }
        } else {
          // This case is redundant for now but in the future, more media might be added, in which case, this conditional
          // will handle it.
          cachingResult = StandardIonResult.getForChemicalAndStandardWellAndNegativeWells(
              lcmsDir, db, chemical, wellToAnalyze, negativeControls, plottingDir, null);
        }
  
        if (cachingResult != null) {
          result.put(wellToAnalyze, cachingResult);
        }
      }
  
      return result;
    }
  
    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)) {
        ScanFile.insertOrUpdateScanFilesInDirectory(db, lcmsDir);
        StandardIonAnalysis analysis = new StandardIonAnalysis();
        HashMap<Integer, Plate> plateCache = new HashMap<>();
  
        String plateBarcode = cl.getOptionValue(OPTION_STANDARD_PLATE_BARCODE);
        String inputChemicals = cl.getOptionValue(OPTION_STANDARD_CHEMICAL);
        String medium = cl.getOptionValue(OPTION_MEDIUM);
  
        // If standard chemical is specified, do standard LCMS ion selection analysis
        if (inputChemicals != null && !inputChemicals.equals("")) {
          String[] chemicals;
          if (!inputChemicals.contains(",")) {
            chemicals = new String[1];
            chemicals[0] = inputChemicals;
          } else {
            chemicals = inputChemicals.split(",");
          }
  
          String outAnalysis = cl.getOptionValue(OPTION_OUTPUT_PREFIX) + "." + CSV_FORMAT;
          String plottingDirectory = cl.getOptionValue(OPTION_PLOTTING_DIR);
          String[] headerStrings = {"Molecule", "Plate Bar Code", "LCMS Detection Results"};
          CSVPrinter printer = new CSVPrinter(new FileWriter(outAnalysis), CSVFormat.DEFAULT.withHeader(headerStrings));
  
          for (String inputChemical : chemicals) {
            List<StandardWell> standardWells;
  
            Plate queryPlate = Plate.getPlateByBarcode(db, cl.getOptionValue(OPTION_STANDARD_PLATE_BARCODE));
            if (plateBarcode != null && medium != null) {
              standardWells = analysis.getStandardWellsForChemicalInSpecificPlateAndMedium(db, inputChemical,
                  queryPlate.getId(), medium);
            } else if (plateBarcode != null) {
              standardWells = analysis.getStandardWellsForChemicalInSpecificPlate(db, inputChemical, queryPlate.getId());
            } else {
              standardWells = analysis.getStandardWellsForChemical(db, inputChemical);
            }
  
            if (standardWells.size() == 0) {
              throw new RuntimeException("Found no LCMS wells for " + inputChemical);
            }
  
            // Sort in descending order of media where MeOH and Water related media are promoted to the top and
            // anything derived from yeast media are demoted.
            Collections.sort(standardWells, new Comparator<StandardWell>() {
              @Override
              public int compare(StandardWell o1, StandardWell o2) {
                if (StandardWell.doesMediaContainYeastExtract(o1.getMedia()) &&
                    !StandardWell.doesMediaContainYeastExtract(o2.getMedia())) {
                  return 1;
                } else {
                  return 0;
                }
              }
            });
  
            Map<StandardWell, StandardIonResult> wellToIonRanking = StandardIonAnalysis.getBestMetlinIonsForChemical(
                    inputChemical, lcmsDir, db, standardWells, plottingDirectory);
  
            if (wellToIonRanking.size() != standardWells.size() && !cl.hasOption(OPTION_OVERRIDE_NO_SCAN_FILE_FOUND)) {
              throw new Exception("Could not find a scan file associated with one of the standard wells");
            }
  
            for (StandardWell well : wellToIonRanking.keySet()) {
              LinkedHashMap<String, XZ> snrResults = wellToIonRanking.get(well).getAnalysisResults();
  
              String snrRankingResults = "";
              int numResultsToShow = 0;
  
              Plate plateForWellToAnalyze = Plate.getPlateById(db, well.getPlateId());
  
              for (Map.Entry<String, XZ> ionToSnrAndTime : snrResults.entrySet()) {
                if (numResultsToShow > 3) {
                  break;
                }
  
                String ion = ionToSnrAndTime.getKey();
                XZ snrAndTime = ionToSnrAndTime.getValue();
  
                snrRankingResults += String.format(ion + " (%.2f SNR at %.2fs); ", snrAndTime.getIntensity(),
                    snrAndTime.getTime());
                numResultsToShow++;
              }
  
              String[] resultSet = {inputChemical,
                  plateForWellToAnalyze.getBarcode() + " " +
                      well.getCoordinatesString() + " " +
                      well.getMedia() + " " +
                      well.getConcentration(),
                  snrRankingResults};
  
              printer.printRecord(resultSet);
            }
          }
  
          try {
            printer.flush();
            printer.close();
          } catch (IOException e) {
            System.err.println("Error while flushing/closing csv writer.");
            e.printStackTrace();
          }
        } else {
          // Get the set of chemicals that includes the construct and all it's intermediates
          Pair<ConstructEntry, List<ChemicalAssociatedWithPathway>> constructAndPathwayChems =
              analysis.getChemicalsForConstruct(db, cl.getOptionValue(OPTION_CONSTRUCT));
          System.out.format("Construct: %s\n", constructAndPathwayChems.getLeft().getCompositionId());
  
          for (ChemicalAssociatedWithPathway pathwayChem : constructAndPathwayChems.getRight()) {
            System.out.format("  Pathway chem %s\n", pathwayChem.getChemical());
  
            // Get all the standard wells for the pathway chemicals. These wells contain only the
            // the chemical added with controlled solutions (ie no organism or other chemicals in the
            // solution)
  
            List<StandardWell> standardWells;
  
            if (plateBarcode != null) {
              Plate queryPlate = Plate.getPlateByBarcode(db, cl.getOptionValue(OPTION_STANDARD_PLATE_BARCODE));
              standardWells = analysis.getStandardWellsForChemicalInSpecificPlate(db, pathwayChem.getChemical(), queryPlate.getId());
            } else {
              standardWells = analysis.getStandardWellsForChemical(db, pathwayChem.getChemical());
            }
  
            for (StandardWell wellToAnalyze : standardWells) {
              List<StandardWell> negativeControls = analysis.getViableNegativeControlsForStandardWell(db, wellToAnalyze);
              Map<StandardWell, List<ScanFile>> allViableScanFiles =
                  analysis.getViableScanFilesForStandardWells(db, wellToAnalyze, negativeControls);
  
              List<String> primaryStandardScanFileNames = new ArrayList<>();
              for (ScanFile scanFile : allViableScanFiles.get(wellToAnalyze)) {
                primaryStandardScanFileNames.add(scanFile.getFilename());
              }
              Plate plate = plateCache.get(wellToAnalyze.getPlateId());
              if (plate == null) {
                plate = Plate.getPlateById(db, wellToAnalyze.getPlateId());
                plateCache.put(plate.getId(), plate);
              }
  
              System.out.format("    Standard well: %s @ %s, '%s'%s%s\n", plate.getBarcode(),
                  wellToAnalyze.getCoordinatesString(),
                  wellToAnalyze.getChemical(),
                  wellToAnalyze.getMedia() == null ? "" : String.format(" in %s", wellToAnalyze.getMedia()),
                  wellToAnalyze.getConcentration() == null ? "" : String.format(" @ %s", wellToAnalyze.getConcentration()));
              System.out.format("      Scan files: %s\n", StringUtils.join(primaryStandardScanFileNames, ", "));
  
              for (StandardWell negCtrlWell : negativeControls) {
                plate = plateCache.get(negCtrlWell.getPlateId());
                if (plate == null) {
                  plate = Plate.getPlateById(db, negCtrlWell.getPlateId());
                  plateCache.put(plate.getId(), plate);
                }
                List<String> negativeControlScanFileNames = new ArrayList<>();
                for (ScanFile scanFile : allViableScanFiles.get(negCtrlWell)) {
                  negativeControlScanFileNames.add(scanFile.getFilename());
                }
  
                System.out.format("      Viable negative: %s @ %s, '%s'%s%s\n", plate.getBarcode(),
                    negCtrlWell.getCoordinatesString(),
                    negCtrlWell.getChemical(),
                    negCtrlWell.getMedia() == null ? "" : String.format(" in %s", negCtrlWell.getMedia()),
                    negCtrlWell.getConcentration() == null ? "" : String.format(" @ %s", negCtrlWell.getConcentration()));
                System.out.format("        Scan files: %s\n", StringUtils.join(negativeControlScanFileNames, ", "));
                // TODO: do something useful with the standard wells and their scan files, and then stop all the printing.
              }
            }
          }
        }
      }
    }
  }
  