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.model.StandardWell;
  import com.act.lcms.plotter.WriteAndPlotMS1Results;
  import org.apache.commons.lang3.tuple.Pair;
  import org.apache.logging.log4j.LogManager;
  import org.apache.logging.log4j.Logger;
  
  import java.io.File;
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  public class ChemicalToMapOfMetlinIonsToIntensityTimeValues {
  
    private static final String FMT = "pdf";
    private static final Logger LOGGER = LogManager.getFormatterLogger(ChemicalToMapOfMetlinIonsToIntensityTimeValues.class);
    private Map<String, Map<String, List<XZ>>> peakData;
  
    protected ChemicalToMapOfMetlinIonsToIntensityTimeValues() {
      this.peakData = new HashMap<>();
    }
  
    public Set<String> getIonList() {
      return this.peakData.keySet();
    }
  
    public Map<String, List<XZ>> getMetlinIonsOfChemical(String chemical) {
      return this.peakData.get(chemical);
    }
  
    public void addIonIntensityTimeValueToChemical(String chemical, String ion, List<XZ> intensityAndTimeValues) {
      Map<String, List<XZ>> val = this.peakData.get(chemical);
      if (val == null) {
        val = new HashMap<>();
      }
      val.put(ion, intensityAndTimeValues);
      this.peakData.put(chemical, val);
    }
  
    private static Double findPeakMaxIntensity(List<XZ> intensityTimeValues) {
      Double maxIntensity = 0.0d;
      for (XZ val : intensityTimeValues) {
        maxIntensity = Math.max(maxIntensity, val.getIntensity());
      }
      return maxIntensity;
    }
  
    /**
     * This function plots the positive ion and negative control ions for a given metlin ion mass per plot.
     * @param searchMz - The mz value which is used for finding spectra.
     * @param plottingDirectory - The directory where the plots will live.
     * @param positiveChemical - The positive chemical is used to make sure it is placed at the top of the spectra plot.
     * @return This function returns a map of ion to absolute paths where the plot lives.
     * @throws IOException
     */
    public Map<String, String> plotPositiveAndNegativeControlsForEachMetlinIon(
        Pair<String, Double> searchMz,
        String plottingDirectory,
        String positiveChemical,
        List<StandardWell> standardWells)
        throws IOException {
      Map<String, String> ionToPlottingFilePath = new HashMap<>();
      Map<String, Double> individualMaxIntensities = new HashMap<>();
      WriteAndPlotMS1Results plottingUtil = new WriteAndPlotMS1Results();
  
      //rearrange the order of plotting
      ArrayList<String> orderedPlotChemicalTitles = new ArrayList<>(this.peakData.keySet().size());
      for (String chemical : peakData.keySet()) {
        if (chemical.equals(positiveChemical)) {
          orderedPlotChemicalTitles.add(0, chemical);
        } else {
          orderedPlotChemicalTitles.add(chemical);
        }
      }
  
      // This variable is used as a part of the file path dir to uniquely identify the pos/neg wells for the chemical.
      StringBuilder indexedPath = new StringBuilder();
      for (StandardWell well : standardWells) {
        indexedPath.append(Integer.toString(well.getId()) + "-");
      }
  
      for (String ion : this.peakData.get(searchMz.getLeft()).keySet()) {
        LinkedHashMap<String, List<XZ>> ms1s = new LinkedHashMap<>();
        Map<String, Double> metlinMasses = new HashMap<>();
        Double maxIntensity = 0.0d;
  
        for (String chemical : orderedPlotChemicalTitles) {
          List<XZ> ionValues = this.peakData.get(chemical).get(ion);
          ms1s.put(chemical, ionValues);
          Double localMaxIntensity = findPeakMaxIntensity(ionValues);
          maxIntensity = Math.max(maxIntensity, localMaxIntensity);
          individualMaxIntensities.put(chemical, localMaxIntensity);
          metlinMasses.put(chemical, searchMz.getValue());
        }
  
        String relativePath = searchMz.getLeft() + "_" + indexedPath.toString() + "_" + ion;
  
        File absolutePathFileWithoutExtension = new File(plottingDirectory, relativePath);
        String absolutePathWithoutExtension = absolutePathFileWithoutExtension.getAbsolutePath();
  
        plottingUtil.plotSpectra(
            ms1s, maxIntensity, individualMaxIntensities, metlinMasses, absolutePathWithoutExtension, this.FMT, false, false);
        ionToPlottingFilePath.put(ion, relativePath + "." + this.FMT);
      }
  
      return ionToPlottingFilePath;
    }
  
    /**
     * This function plots a combination of positive and negative control intensity-time values.
     * @param searchMzs A list of mass charge values
     * @param plottingPath The wells used for the analysis. This variable is mainly used for
     * @param peakDataPos The postive intensity-time value
     * @param peakDataNegs The negative controls intensity-time values
     * @param plottingDirectory The directory where the plots are going to be placed in
     * @return
     * @throws IOException
     */
    public static Map<String, String> plotPositiveAndNegativeControlsForEachMZ(
        Set<Pair<String, Double>> searchMzs, String plottingPath, ChemicalToMapOfMetlinIonsToIntensityTimeValues peakDataPos,
        List<ChemicalToMapOfMetlinIonsToIntensityTimeValues> peakDataNegs, String plottingDirectory)
        throws IOException {
  
      Map<String, String> result = new HashMap<>();
      Map<String, Double> individualMaxIntensities = new HashMap<>();
      WriteAndPlotMS1Results plottingUtil = new WriteAndPlotMS1Results();
  
      for (Pair<String, Double> mz : searchMzs) {
        LinkedHashMap<String, List<XZ>> ms1s = new LinkedHashMap<>();
        Map<String, Double> metlinMasses = new HashMap<>();
        Double maxIntensity = 0.0d;
  
        String chemicalAndIonName = mz.getLeft();
        Double massChargeValue = mz.getRight();
  
        // Get positive ion results
        String positiveChemicalName = AnalysisHelper.constructChemicalAndScanTypeName(chemicalAndIonName, ScanData.KIND.POS_SAMPLE);
        List<XZ> ionValuesPos = peakDataPos.peakData.get(positiveChemicalName).get(chemicalAndIonName);
        ms1s.put(positiveChemicalName, ionValuesPos);
        Double localMaxIntensityPos = findPeakMaxIntensity(ionValuesPos);
        maxIntensity = Math.max(maxIntensity, localMaxIntensityPos);
        individualMaxIntensities.put(positiveChemicalName, localMaxIntensityPos);
        metlinMasses.put(positiveChemicalName, massChargeValue);
  
        // Get negative control results
        Integer negNameCounter = 0;
        for (ChemicalToMapOfMetlinIonsToIntensityTimeValues peakDataNeg : peakDataNegs) {
          String negativeChemicalName = AnalysisHelper.constructChemicalAndScanTypeName(chemicalAndIonName, ScanData.KIND.NEG_CONTROL);
          String negativeChemicalNameId = negativeChemicalName + "_" + negNameCounter.toString();
          List<XZ> ionValuesNeg = peakDataNeg.peakData.get(negativeChemicalName).get(chemicalAndIonName);
          ms1s.put(negativeChemicalNameId, ionValuesNeg);
          Double localMaxIntensityNeg = findPeakMaxIntensity(ionValuesNeg);
          maxIntensity = Math.max(maxIntensity, localMaxIntensityNeg);
          individualMaxIntensities.put(negativeChemicalNameId, localMaxIntensityNeg);
          metlinMasses.put(negativeChemicalNameId, massChargeValue);
          negNameCounter++;
        }
  
        String relativePath = massChargeValue.toString() + "_" + plottingPath + "_" + chemicalAndIonName;
        File absolutePathFileWithoutExtension = new File(plottingDirectory, relativePath);
        String absolutePathWithoutExtension = absolutePathFileWithoutExtension.getAbsolutePath();
        String absolutePathWithExtension = absolutePathWithoutExtension + "." + FMT;
  
        // Check if the plotting file already exists. If it does, we should not overwrite it. Instead, we just change
        // the path name by appending a counter till the collision no longer exists.
        // TODO: Implement an elegant solution to this problem.
        File duplicateFile = new File(absolutePathWithExtension);
        Integer fileDuplicateCounter = 0;
        while (duplicateFile.exists() && !duplicateFile.isDirectory()) {
          LOGGER.warn("Duplicate file exists for %s, writing to another file", duplicateFile.getAbsolutePath());
          fileDuplicateCounter++;
          relativePath = relativePath + "_" + fileDuplicateCounter.toString();
          absolutePathFileWithoutExtension = new File(plottingDirectory, relativePath);
          absolutePathWithoutExtension = absolutePathFileWithoutExtension.getAbsolutePath();
          absolutePathWithExtension = absolutePathWithoutExtension + "." + FMT;
          duplicateFile = new File(absolutePathWithExtension);
        }
  
        LOGGER.info("Wrote plot to %s", absolutePathWithoutExtension);
  
        plottingUtil.plotSpectra(
            ms1s, maxIntensity, individualMaxIntensities, metlinMasses, absolutePathWithoutExtension, FMT, false, false);
  
        result.put(mz.getLeft(), relativePath + "." + FMT);
      }
  
      return result;
    }
  }
  