/* 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/filters/filterresample.h>
#include <pappsomspp/processing/filters/filterpass.h>


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


namespace msxps
{
namespace minexpert
{


MsRunStatisticsTreeNodeVisitor::MsRunStatisticsTreeNodeVisitor(
  MsRunDataSetCstSPtr ms_run_data_set_csp,
  const ProcessingFlow &processing_flow,
  const MsRunDataSetStats &ms_run_data_set_stats)
  : BaseMsRunDataSetTreeNodeVisitor(ms_run_data_set_csp, processing_flow),
    m_msRunDataSetStats(ms_run_data_set_stats)
{
  //  qDebug() << "Processing flow has" << m_processingFlow.size() << "steps";
}


MsRunStatisticsTreeNodeVisitor::MsRunStatisticsTreeNodeVisitor(
  const MsRunStatisticsTreeNodeVisitor &other)
  : BaseMsRunDataSetTreeNodeVisitor(other),
    m_msRunDataSetStats(other.m_msRunDataSetStats)
{
}


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

  BaseMsRunDataSetTreeNodeVisitor::operator=(other);

  mcsp_msRunDataSet   = other.mcsp_msRunDataSet;
  m_processingFlow    = other.m_processingFlow;
  m_msRunDataSetStats = other.m_msRunDataSetStats;

  return *this;
}


MsRunStatisticsTreeNodeVisitor::~MsRunStatisticsTreeNodeVisitor()
{
  // qDebug();
}


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


void
MsRunStatisticsTreeNodeVisitor::setLimitMzRangeStart(double value)
{
  m_limitMzRangeStart = value;
}


void
MsRunStatisticsTreeNodeVisitor::setLimitMzRangeEnd(double value)
{
  m_limitMzRangeEnd = value;
}


const MsRunDataSetStats &
MsRunStatisticsTreeNodeVisitor::getMsRunDataSetStats() const
{
  return m_msRunDataSetStats;
}


bool
MsRunStatisticsTreeNodeVisitor::visit(const pappso::MsRunDataSetTreeNode &node)
{

  // Each time we visit a node, we check that it matches all the requirements in
  // the ProcessingFlow. Then, if that is the case, we aggregrate its statistics
  // to the MsRunDataSets member datum.

  // qDebug() << "Visiting node:" << node.toString() << "from thread:" <<
  // QThread::currentThread(); qDebug() << "Visiting node from thread:" <<
  // QThread::currentThread();

  // 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");

  // 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;
        }
    }

  // At this point we know that the current node matches all the flow's steps'
  // specs.

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

  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.");
    }

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

  pappso::MassSpectrum &mass_spectrum =
    *(qualified_mass_spectrum_csp->getMassSpectrumSPtr());

  // qDebug() << "The mass spectrum being visited has size:"
  //<< mass_spectrum.size();

  if(m_limitMzRangeStart != std::numeric_limits<double>::max() &&
     m_limitMzRangeEnd != std::numeric_limits<double>::max())
    {
      // Before analyzing statistically the mass spectrum, we need to check
      // that we work on the proper mz range asked by the caller. Here we need
      // to filter the mass spectrum to only keep the m/z range specified.

      pappso::FilterResampleKeepXRange filter;
      pappso::MassSpectrum filtered_mass_spectrum =
        filter.filter(mass_spectrum);

      // qDebug() << "The filtered mass spectrum has size:"
      //<< filtered_mass_spectrum.size();

      m_msRunDataSetStats.incrementStatistics(filtered_mass_spectrum);
    }
  else
    {
      // We do not want to bother with y=0 data points !
      pappso::FilterHighPass filter(0.0);

      pappso::MassSpectrum filtered_mass_spectrum =
        filter.filter(mass_spectrum);

      // qDebug() << "The filtered mass spectrum has size:"
      //<< filtered_mass_spectrum.size()
      //<< "with first data point:"
      //<< filtered_mass_spectrum.front().toString();

      m_msRunDataSetStats.incrementStatistics(filtered_mass_spectrum);
    }

  // qDebug().noquote() << "statistics:" << m_msRunDataSetStats.toString();

  return true;
}


bool
MsRunStatisticsTreeNodeVisitor::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
MsRunStatisticsTreeNodeVisitor::checkProcessingSpec(
  const std::pair<ProcessingType, ProcessingSpec *> &type_spec_pair,
  const pappso::MsRunDataSetTreeNode &node)
{
  //  qDebug();

  // This is the most 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.isValid())
    {
      // qDebug() << "The fragmentation spec *is* valid.";

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

      if(fragmentation_spec.getMsLevel())
        {
          if(qualified_mass_spectrum_csp->getMsLevel() <
             fragmentation_spec.getMsLevel())
            {
              // qDebug() << "The ms level does not match.";
              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() << "Frag spec ok, going on with the spec check.";

  ProcessingType type = type_spec_pair.first;

  // Because we are working on the TIC chromatogram visitor, we need to match
  // the corresponding processing type that creates a TIC chromatogram right
  // from the data file, apart from other processes that create a TIC chrom
  // from other data settings, which in truth are called XIC chrom.

  if(type.bitMatches("FILE_TO_RT") || 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
MsRunStatisticsTreeNodeVisitor::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;

  if(spec_p->hasValidRange())
    {
      double rt = qualified_mass_spectrum_csp->getRtInMinutes();

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

  return true;
}


bool
MsRunStatisticsTreeNodeVisitor::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
      // processing 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_limitMzRangeStart = local_type_spec_pair.second->getStart();
          m_limitMzRangeEnd   = 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 because we'll account that node whatever the mz range
  // criterion.

  return true;
}


bool
MsRunStatisticsTreeNodeVisitor::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;

  if(type_spec_pair.second->hasValidRange())
    {
      double dt = qualified_mass_spectrum_csp->getDtInMilliSeconds();

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

  return true;
}


} // namespace minexpert

} // namespace msxps
