/* BEGIN software license
 *
 * msXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright(C) 2009,...,2019 Filippo Rusconi
 *
 * http://www.msxpertsuite.org
 *
 * This file is part of the msXpertSuite project.
 *
 * The msXpertSuite project is the successor of the massXpert project. This
 * project now includes various independent modules:
 *
 * - massXpert, model polymer chemistries and simulate mass spectrometric data;
 * - mineXpert, a powerful TIC chromatogram/mass spectrum viewer/miner;
 *
 * 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/>.
 *
 * END software license
 */


/////////////////////// StdLib includes


/////////////////////// Qt includes
#include <QDebug>
#include <QThread>


/////////////////////// pappsomspp includes
#include "pappsomspp/processing/combiners/tracepluscombiner.h"


/////////////////////// Local includes
#include "RtDtColorMapTreeNodeCombinerVisitor.hpp"


namespace msxps
{
namespace minexpert
{


RtDtColorMapTreeNodeCombinerVisitor::RtDtColorMapTreeNodeCombinerVisitor(
  MsRunDataSetCstSPtr ms_run_data_set_csp,
  const ProcessingFlow &processing_flow,
  pappso::DataKind data_kind)
  : BaseMsRunDataSetTreeNodeVisitor(ms_run_data_set_csp, processing_flow),
    m_dataKind(data_kind)
{
  //  qDebug() << "Processing flow has" << m_processingFlow.size() << "steps";

  if(data_kind != pappso::DataKind::rt && data_kind != pappso::DataKind::dt)
    qFatal("Programming error.");
}


RtDtColorMapTreeNodeCombinerVisitor::RtDtColorMapTreeNodeCombinerVisitor(
  const RtDtColorMapTreeNodeCombinerVisitor &other)
  : BaseMsRunDataSetTreeNodeVisitor(other),
    m_dataKind(other.m_dataKind),
    m_combiner(other.m_combiner),
    m_doubleMapTraceMap(other.m_doubleMapTraceMap)
{
}


RtDtColorMapTreeNodeCombinerVisitor &
RtDtColorMapTreeNodeCombinerVisitor::operator=(
  const RtDtColorMapTreeNodeCombinerVisitor &other)
{
  if(this == &other)
    return *this;

  BaseMsRunDataSetTreeNodeVisitor::operator=(other);

  m_dataKind          = other.m_dataKind;
  m_doubleMapTraceMap = other.m_doubleMapTraceMap;

  m_combiner = other.m_combiner;

  return *this;
}


RtDtColorMapTreeNodeCombinerVisitor::~RtDtColorMapTreeNodeCombinerVisitor()
{
}


void
RtDtColorMapTreeNodeCombinerVisitor::setProcessingFlow(
  const ProcessingFlow &processing_flow)
{
  m_processingFlow = processing_flow;
}


const std::map<double, pappso::MapTrace> &
RtDtColorMapTreeNodeCombinerVisitor::getDoubleMapTraceMap() const
{
  return m_doubleMapTraceMap;
}


pappso::DataKind
RtDtColorMapTreeNodeCombinerVisitor::getDataKind() const
{
  return m_dataKind;
}


bool
RtDtColorMapTreeNodeCombinerVisitor::visit(
  const pappso::MsRunDataSetTreeNode &node)
{
  // qDebug() << "Visiting node:" << node.toString();

  // In this visitor, we are creating data to populate a DT/RT or RT/DT colormap
  // where each point has a color matching the TIC intensity of the data at that
  // point.

  // We receive on DataKind that is the first key, for example RT. In that case
  // we deduce that the other key is DT. And vice versa.

  // Sanity check
  if(m_dataKind != pappso::DataKind::rt && m_dataKind != pappso::DataKind::dt)
    qFatal("Programming error.");

  // qDebug() << "Visiting node for mass data kind:" << (int)m_dataKind;

  if(!m_isProgressFeedbackSilenced)
    {
      // Whatever the status of this node (to be or not processed) let the user
      // know that we have gone through one more.

      // The "%c" format refers to the current value in the task monitor widget
      // that will craft the new text according to the current value after
      // having incremented it in the call below. This is necessary because this
      // visitor might be in a thread and the  we need to account for all the
      // thread when giving feedback to the user.

      emit incrementProgressBarCurrentValueAndSetStatusTextSignal(
        1, "Processed %c nodes");
    }

  // This visit() function handles the complex data acquired during IM-MS
  // experiments. In this kind of experiment, a single scan start time (that is
  // the retention time value) can be associated with many mass spectra, because
  // each mobility mass spectrum (on Synapt instruments, 200 mobility spectra
  // are acquired) is acquired for that rt value. Many more so with the timsTOF
  // Bruker instrument.

  // This function can be used to handle rt/mass spectra or dt/mass spectra
  // association depending on the way *this object was constructed.

  // Immediately get a pointer to  the qualified mass spectrum that is enshrined
  // in the node.

  // qDebug() << "Visiting node:" << &node << "text format:" << node.toString()
  //<< "with quaified mass spectrum:"
  //<< node.getQualifiedMassSpectrum()->toString();

  pappso::QualifiedMassSpectrumCstSPtr qualified_mass_spectrum_csp =
    node.getQualifiedMassSpectrum();

  // A processing step might have a valid fragmentation specification. We need
  // to test that immediately before delving into the steps that make this flow.

  if(m_processingFlow.getDefaultMsFragmentationSpec().getMsLevel())
    {
      // qDebug() << "Node:" << &node
      //<< "The default ms fragmentation spec has ms level:"
      //<< m_processingFlow.getDefaultMsFragmentationSpec()
      //.getGreatestMsLevel();

      if(qualified_mass_spectrum_csp->getMsLevel() >=
         m_processingFlow.getDefaultMsFragmentationSpec().getMsLevel())
        {
          // qDebug() << "Node:" << &node << "The default frag spec ms level:"
          //<< m_processingFlow.getDefaultMsFragmentationSpec()
          //.getGreatestMsLevel()
          //<< "does NOT match the spectrum ms level:"
          //<< qualified_mass_spectrum_csp->getMsLevel();

          // The default ms fragmentation step's ms level in the processing flow
          // does not match the ms level of the current mass spectrum. Return
          // false immediately.

          return false;
        }
      else
        {
          // qDebug() << "Node:" << &node << "The default frag spec ms level:"
          //<< m_processingFlow.getDefaultMsFragmentationSpec()
          //.getGreatestMsLevel()
          //<< "matches YES the spectrum ms level:"
          //<< qualified_mass_spectrum_csp->getMsLevel();
        }
    }
  else
    {
      // qDebug() << "The processing flow's default ms frag spec is not valid.";
    }

  // At this point we need to go deep in the processing flow's steps to check if
  // the node matches the whole set of the steps' specs.

  for(auto &&step : m_processingFlow)
    {
      if(checkProcessingStep(*step, node))
        {
          // qDebug() << "The node matches the iterated step.";
        }
      else
        {
          // qDebug() << "The node does not match the iterated step.";

          return false;
        }
    }

  if(qualified_mass_spectrum_csp == nullptr)
    qFatal("Cannot be nullptr");
  if(qualified_mass_spectrum_csp.get() == nullptr)
    qFatal("Cannot be nullptr");

  if(qualified_mass_spectrum_csp->getMassSpectrumSPtr() == nullptr)
    {
      // qDebug() << "Need to access the mass data right from the file.";

      std::size_t mass_spectrum_index =
        mcsp_msRunDataSet->getMsRunDataSetTreeCstSPtr()->massSpectrumIndex(
          &node);

      pappso::MsRunReaderCstSPtr ms_run_reader_csp =
        mcsp_msRunDataSet->getMsRunReaderCstSPtr();

      qualified_mass_spectrum_csp =
        std::make_shared<pappso::QualifiedMassSpectrum>(
          ms_run_reader_csp->qualifiedMassSpectrum(mass_spectrum_index, true));
    }

  if(qualified_mass_spectrum_csp == nullptr)
    {
      qFatal("Failed to read the mass spectral data from the file.");
    }

  // if(!qualified_mass_spectrum_csp->size())
  // qDebug() << "The mass spectrum is empty.";

  // qDebug() << "The qualified mass spectrum:"
  //<< qualified_mass_spectrum_csp->toString()
  //<< "rt in minutes:" << qualified_mass_spectrum_csp->getRtInMinutes();

  // At this point, we need to cope with the data. What we want is to combine
  // into a single mass spectrum all the MS1 mass spectra that have been
  // acquired for any given retention time (on Synapt machines, that would be
  // 200 mobility spectra for each given rt value, year 2018).

  double first_key  = 0;
  double second_key = 0;

  if(m_dataKind == pappso::DataKind::rt)
    {
      first_key  = qualified_mass_spectrum_csp->getRtInMinutes();
      second_key = qualified_mass_spectrum_csp->getDtInMilliSeconds();
    }
  else
    {
      first_key  = qualified_mass_spectrum_csp->getDtInMilliSeconds();
      second_key = qualified_mass_spectrum_csp->getRtInMinutes();
    }

  // We want to know what is the TIC intensity value of the spectrum.
  double mass_spectrum_sum_y =
    qualified_mass_spectrum_csp->getMassSpectrumSPtr()->sumY();

  // When doing IM-MS experiments, the drift time of the ions introduces a new
  // dimension in the mass data when compared with conventional mass
  // spectrometry. In this function we try to cope with this extra dimension.

  // The m_doubleMapTraceMap is a <rt|dt, MapTrace> map that relates a MapTrace
  // to either a retention time or a drift time, depending on the kind of
  // m_dataKind. The MapTrace is not a mass spectral trace, it is an object that
  // relates two <double, double> values: the other value (if m_dataKind is RT,
  // then the other value is DT and vice versa) and the intensity of the mass
  // data at that point, that is the TIC of the mass spectrum acquired at that
  // point.

  std::map<double, pappso::MapTrace>::iterator double_maptrace_map_iterator;
  double_maptrace_map_iterator = m_doubleMapTraceMap.find(first_key);

  // Tells if the key was already found in the map.
  if(double_maptrace_map_iterator != m_doubleMapTraceMap.end())
    {

      // qDebug() << "A node for a mass spectrum having that same rt_or_dt_key "
      //"was found already:"<< rt_or_dt_key;

      // The map already contained a pair that had the key value ==
      // first_key. Let's say that m_dataKind is DT. We alreay saw data acquired
      // at DT. So other_key holds the RT of the mass spectrum.  All this can be
      // vice-versa'd.

      using MapIterator = std::map<double, double>::iterator;

      // Insert the <double, double> pair as <rt|dt, TICint> in the MapTrace
      // matching first_key.

      std::pair<MapIterator, bool> res =
        double_maptrace_map_iterator->second.insert(
          std::pair<double, double>(second_key, mass_spectrum_sum_y));

      if(!res.second)
        {
          // One other same second_key value was seen already. Only
          // increment the y value.

          res.first->second += mass_spectrum_sum_y;
        }
    }
  else
    {

      // No other mass spectrum was encountered by the rt_or_dt_key key. So we
      // need to do all the process here.

      // qDebug() << "No node for a mass spectrum having that same rt_or_dt_key
      // " "was found already:"
      //<< rt_or_dt_key;

      // Instantiate a new MapTrace in which we'll combine this and all the
      // future mass spectra having been acquired at this same dt value.

      pappso::MapTrace map_trace;

      map_trace.insert(
        std::pair<double, double>(second_key, mass_spectrum_sum_y));

      // Finally, store in the map<double, MapTrace> map, the map_trace
      // along with its first_key.

      m_doubleMapTraceMap[first_key] = map_trace;
    }

  return true;
}


bool
RtDtColorMapTreeNodeCombinerVisitor::checkProcessingStep(
  const ProcessingStep &step, const pappso::MsRunDataSetTreeNode &node)
{
  //  qDebug();

  // A processing step is a collection of mapped items:
  // std::map<ProcessingType, ProcessingSpec> m_processingTypeSpecMap;
  //
  // For each item, we need to check if the node matches the spec.

  for(auto &&pair : step.getProcessingTypeSpecMap())
    {
      if(checkProcessingSpec(pair, node))
        {
          //          qDebug() << "The node matches the iterated spec.";
        }
      else
        {
          //          qDebug() << "The node does not match the iterated
          //          spec.";

          return false;
        }
    }

  return true;
}


bool
RtDtColorMapTreeNodeCombinerVisitor::checkProcessingSpec(
  const std::pair<ProcessingType, ProcessingSpec *> &type_spec_pair,
  const pappso::MsRunDataSetTreeNode &node)
{
  //  qDebug();

  // This is the elemental component of a ProcessingFlow, so here we actually
  // look into the node.

  const ProcessingSpec *spec_p = type_spec_pair.second;

  pappso::QualifiedMassSpectrumCstSPtr qualified_mass_spectrum_csp =
    node.getQualifiedMassSpectrum();

  const MsFragmentationSpec fragmentation_spec =
    spec_p->getMsFragmentationSpec();

  if(fragmentation_spec.getMsLevel())
    {
      // qDebug() << "The fragmentation spec *is* valid.";

      // For each member of the MsFragmentationSpec structure, check if it is to
      // be used for the check.


      // Logically, if the currently handled mass spectrum has a ms level
      // that is greater than the fragmentation spec ms level, then that
      // means that it should be visited: in the process of making data
      // mining, one goes from the lowest ms level to higher ms levels, so
      // we need to keep the mass spectrum. At some point down the
      // processing datetime, a process spec will limit the mass spectra
      // that are actually validated.

      if(qualified_mass_spectrum_csp->getMsLevel() >=
         fragmentation_spec.getMsLevel())
        {
          // qDebug() << "Node:" << &node
          //<< "The ms level matches, with fragspec level:"
          //<< fragmentation_spec.msLevelsToString()
          //<< "and the node spectrum level:"
          //<< qualified_mass_spectrum_csp->getMsLevel();
        }
      else
        {
          // qDebug() << "Node:" << &node
          //<< "The ms level does not match, with fragspec level:"
          //<< fragmentation_spec.msLevelsToString()
          //<< "and the node spectrum level:"
          //<< qualified_mass_spectrum_csp->getMsLevel();

          return false;
        }

      if(fragmentation_spec.precursorMzValuesCount())
        {
          const std::vector<pappso::PrecursorIonData>
            &precursor_ion_data_vector =
              qualified_mass_spectrum_csp->getPrecursorIonData();

          if(!fragmentation_spec.containsMzPrecursors(
               precursor_ion_data_vector))
            {
              return false;
            }
        }

      if(fragmentation_spec.precursorSpectrumIndicesCount())
        {
          if(!fragmentation_spec.containsSpectrumPrecursorIndex(
               qualified_mass_spectrum_csp->getPrecursorSpectrumIndex()))
            {
              // qDebug() << "The precursor spectrum index does not match.";

              return false;
            }
        }
    }
  else
    {
      // Else, fragmentation is not a criterion for filtering data. So go on.
      // qDebug() << "The fragmentation spec is *not* valid.";
    }

  // qDebug() << "Going on with the spec check.";

  ProcessingType type = type_spec_pair.first;

  if(type.bitMatches("RT_TO_ANY"))
    {
      //      qDebug();
      if(!checkProcessingTypeByRt(type_spec_pair, node))
        return false;
    }
  else if(type.bitMatches("MZ_TO_ANY"))
    {
      //      qDebug();
      if(!checkProcessingTypeByMz(type_spec_pair, node))
        return false;
    }
  else if(type.bitMatches("DT_TO_ANY"))
    {
      //      qDebug();
      if(!checkProcessingTypeByDt(type_spec_pair, node))
        return false;
    }

  // At this point we seem to understand that the node matched!

  return true;
}


bool
RtDtColorMapTreeNodeCombinerVisitor::checkProcessingTypeByRt(
  const std::pair<ProcessingType, ProcessingSpec *> &type_spec_pair,
  const pappso::MsRunDataSetTreeNode &node)
{
  //  qDebug();

  pappso::QualifiedMassSpectrumCstSPtr qualified_mass_spectrum_csp =
    node.getQualifiedMassSpectrum();

  const ProcessingSpec *spec_p = type_spec_pair.second;

  double rt = qualified_mass_spectrum_csp->getRtInMinutes();

  if(rt >= spec_p->getStart() && rt <= spec_p->getEnd())
    {
      //      qDebug() << "Returning true for Rt.";
      return true;
    }

  return false;
}


bool
RtDtColorMapTreeNodeCombinerVisitor::checkProcessingTypeByMz(
  const std::pair<ProcessingType, ProcessingSpec *> &type_spec_pair,
  [[maybe_unused]] const pappso::MsRunDataSetTreeNode &node)
{
  qDebug();

  // Are there specs about the m/z range to be accounted for in the
  // computation?

  const ProcessingSpec *spec_p = type_spec_pair.second;

  if(spec_p->hasValidRange())
    {
      qDebug() << "Needed a m/z range filtering:" << spec_p->toString();

      // The point is that if there are more than one spec in the the flow's
      // steps that limit the m/z range, we want to be sure to perform the
      // combination using the innermost range. So ask the ProcessingFlow to
      // actually compute that innermost m/z range.

      // Make a local copy of the type_spec_pair
      std::pair<ProcessingType, ProcessingSpec *> local_type_spec_pair(
        type_spec_pair);

      if(m_processingFlow.innermostMzRange(local_type_spec_pair))
        {
          m_combiner.setFilterResampleKeepXRange(
            pappso::FilterResampleKeepXRange(
              local_type_spec_pair.second->getStart(),
              local_type_spec_pair.second->getEnd()));
        }
      else
        qFatal(
          "Cannot be that there is no innermost m/z range if there is a valid "
          "m/z range in a spec.");
    }

  return true;
}


bool
RtDtColorMapTreeNodeCombinerVisitor::checkProcessingTypeByDt(
  const std::pair<ProcessingType, ProcessingSpec *> &type_spec_pair,
  const pappso::MsRunDataSetTreeNode &node)
{
  //  qDebug();

  pappso::QualifiedMassSpectrumCstSPtr qualified_mass_spectrum_csp =
    node.getQualifiedMassSpectrum();

  const ProcessingSpec *spec_p = type_spec_pair.second;

  double dt = qualified_mass_spectrum_csp->getDtInMilliSeconds();

  if(dt >= spec_p->getStart() && dt <= spec_p->getEnd())
    return true;

  return false;
}


} // namespace minexpert

} // namespace msxps
