/* BEGIN software license
 *
 * msXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright (C) 2009--2020 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
#include <map>
#include <cmath>
#include <vector>
#include <set>
#include <algorithm>


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


/////////////////////// pappsomspp includes
#include <pappsomspp/massspectrum/qualifiedmassspectrum.h>
#include <pappsomspp/processing/combiners/mzintegrationparams.h>


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


int msRunDataSetMetaTypeId = qRegisterMetaType<msxps::minexpert::MsRunDataSet>(
  "msxps::minexpert::MsRunDataSet");

int msRunDataSetPtrMetaTypeId =
  qRegisterMetaType<msxps::minexpert::MsRunDataSet *>(
    "msxps::minexpert::MsRunDataSetPtr");

int msRunDataSetSPtrMetaTypeId =
  qRegisterMetaType<msxps::minexpert::MsRunDataSetSPtr>(
    "msxps::minexpert::MsRunDataSetSPtr");

int msRunDataSetCstSPtrMetaTypeId =
  qRegisterMetaType<msxps::minexpert::MsRunDataSetCstSPtr>(
    "msxps::minexpert::MsRunDataSetCstSPtr");


namespace msxps
{
namespace minexpert
{


MsRunDataSet::MsRunDataSet()
{
}


MsRunDataSet::MsRunDataSet(QObject *parent,
                           const pappso::MsRunReaderSPtr ms_run_reader_sp,
                           bool isStreamed)
  : QObject(parent), msp_msRunReader(ms_run_reader_sp), m_isStreamed(isStreamed)
{
  // qDebug() << "ms run id:" << ms_run_reader_sp->getMsRunId()->toString();

  msp_msRunDataSetTree =
    std::make_shared<pappso::MsRunDataSetTree>(ms_run_reader_sp->getMsRunId());
}


MsRunDataSet::MsRunDataSet(const MsRunDataSet &other)
  : QObject(other.parent()),
    msp_msRunReader(other.msp_msRunReader),
    m_isStreamed(other.m_isStreamed)
{
}


//! Destruct \c this MsRunDataSet instance.
/*!

  The data in \c this instance are owned by this instance. They thus should be
  deleted.

  \sa deleteSpectra().

*/
MsRunDataSet::~MsRunDataSet()
{
  // qDebug() << "In the destructor of MsRunDataSet:" << this;
}


//! Get a const reference to the mass spectra data set statistics.
const MsRunDataSetStats &
MsRunDataSet::getMsRunDataSetStats() const
{
  return m_msRunDataSetStats;
}


void
MsRunDataSet::setMsRunDataSetStats(
  const MsRunDataSetStats &ms_run_data_set_stats)
{
  m_msRunDataSetStats = ms_run_data_set_stats;
}


//! Tells if the data must be or were loaded from file in streamed mode.
bool
MsRunDataSet::isIonMobilityExperiment() const
{
  return m_dtMsRunDataSetTreeNodeMap.size();
}


void
MsRunDataSet::setFileFormat(pappso::MzFormat file_format)
{
  m_fileFormat = file_format;
}


pappso::MzFormat
MsRunDataSet::getFileFormat() const
{
  return m_fileFormat;
}


bool
MsRunDataSet::isStreamed() const
{
  return m_isStreamed;
}


pappso::MsRunReaderSPtr
MsRunDataSet::getMsRunReaderSPtr()
{
  return msp_msRunReader;
}


pappso::MsRunReaderCstSPtr
MsRunDataSet::getMsRunReaderCstSPtr() const
{
  return msp_msRunReader;
}


pappso::MsRunIdCstSPtr
MsRunDataSet::getMsRunId() const
{
  return msp_msRunReader->getMsRunId();
}


void
MsRunDataSet::resetStatistics()
{
  m_msRunDataSetStats.reset();
}


const MsRunDataSetStats &
MsRunDataSet::incrementStatistics(
  const pappso::MassSpectrumCstSPtr massSpectrum)
{
  m_msRunDataSetStats.incrementStatistics(massSpectrum);
  return m_msRunDataSetStats;
}


const MsRunDataSetStats &
MsRunDataSet::consolidateStatistics()
{
  m_msRunDataSetStats.consolidate();
  return m_msRunDataSetStats;
}


QString
MsRunDataSet::statisticsToString() const
{
  return m_msRunDataSetStats.toString();
}


pappso::MsRunDataSetTreeCstSPtr
MsRunDataSet::getMsRunDataSetTreeCstSPtr() const
{
  return msp_msRunDataSetTree;
}


pappso::MzIntegrationParams
MsRunDataSet::craftInitialMzIntegrationParams() const
{
  return pappso::MzIntegrationParams(
    // Absolute min m/z value of the whole data set
    m_msRunDataSetStats.m_minMz,
    // Absolute max m/z value of the whole data set
    m_msRunDataSetStats.m_maxMz,
    // Binning type
    pappso::BinningType::ARBITRARY,
    // Decimal places
    3,
    // The size of the m/z bins in Dalton units.
    pappso::PrecisionFactory::getDaltonInstance(
      m_msRunDataSetStats.m_smallestStepMedian),

    // Should apply the m/z shift
    false,
    // The m/z shift to apply
    0.0,
    // Remove or not zero val data points
    true);
}


//! Return the number of mass spectra in \c this MsRunDataSet instance.
int
MsRunDataSet::size() const
{
  return msp_msRunDataSetTree->size();
  return 1;
}


//! Set the parent object of \c this MsRunDataSet instance.
void
MsRunDataSet::setParent(QObject *parent)
{
  QObject::setParent(parent);
}


void
MsRunDataSet::mapMassSpectrum(
  pappso::QualifiedMassSpectrumCstSPtr qualified_mass_spectrum_csp)
{
  // qDebug();

  // We need to store the mass spectrum. The mass spec data tree has functions
  // to do that.

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

  pappso::MsRunDataSetTreeNode *new_node_p =
    msp_msRunDataSetTree->addMassSpectrum(qualified_mass_spectrum_csp);

  // At this point, finish the mapping with the retention time and drift time
  // (if applicable)

  // We want to standardize retention time to be in min
  double rt = qualified_mass_spectrum_csp->getRtInMinutes();
  if(rt == -1)
    qFatal("Programming error.");

  // A single rt value in the map is associated to a list of ms run
  // data set tree nodes.

  using map_iterator = MapDoubleMsRunDataSetTreeNode::iterator;

  map_iterator found_iter = m_rtMsRunDataSetTreeNodeMap.find(rt);

  if(found_iter == m_rtMsRunDataSetTreeNodeMap.end())
    {
      // This is the first time that we encounter rt.

      std::vector<pappso::MsRunDataSetTreeNode *> new_node_vector;
      new_node_vector.push_back(new_node_p);

      m_rtMsRunDataSetTreeNodeMap.insert(
        std::pair<double, std::vector<pappso::MsRunDataSetTreeNode *>>(
          rt, new_node_vector));
    }
  else
    {
      // A rt value was encountered already, thus push back the new mass
      // spectrum to the corresponding vector of qualified mass spectra (second
      // member of the pair).
      found_iter->second.push_back(new_node_p);
    }

  // The drift time is always in milliseconds
  double dt = qualified_mass_spectrum_csp->getDtInMilliSeconds();

  if(dt != -1)
    {
      // A single dt value in the map is associated to a list of ms
      // run data set tree nodes.

      using map_iterator = MapDoubleMsRunDataSetTreeNode::iterator;

      map_iterator found_iter = m_dtMsRunDataSetTreeNodeMap.find(dt);

      if(found_iter == m_dtMsRunDataSetTreeNodeMap.end())
        {
          // This is the first time that we encounter dt.

          std::vector<pappso::MsRunDataSetTreeNode *> new_node_vector;
          new_node_vector.push_back(new_node_p);

          m_dtMsRunDataSetTreeNodeMap.insert(
            std::pair<double, std::vector<pappso::MsRunDataSetTreeNode *>>(
              dt, new_node_vector));
        }
      else
        {
          // A dt value was encountered already, thus push back the new mass
          // spectrum to the corresponding vector of qualified mass spectra
          // (second member of the pair).
          found_iter->second.push_back(new_node_p);
        }
    }
}


//! Return mass spectrum at index \p index.
pappso::QualifiedMassSpectrumCstSPtr
MsRunDataSet::massSpectrumAtIndex(std::size_t spectrum_index)
{
  pappso::MsRunDataSetTreeNode *node =
    msp_msRunDataSetTree->findNode(spectrum_index);

  if(node != nullptr)
    return node->getQualifiedMassSpectrum();

  return nullptr;
}


using VectorConstIterator =
  std::vector<pappso::MsRunDataSetTreeNode *>::const_iterator;

std::pair<VectorConstIterator, VectorConstIterator>
MsRunDataSet::treeNodeIteratorRangeForRtRange(double start, double end) const
{
  qDebug().noquote() << "Determining iterators in the root nodes vector for "
                        "retention time range ["
                     << start << "," << end << "]";

  // Get a pointer to the root nodes of the data set tree.

  const std::vector<pappso::MsRunDataSetTreeNode *> &data_set_tree_root_nodes =
    msp_msRunDataSetTree->getRootNodes();

  // Locate in the tree node, the node that has the mass spectrum acquired at
  // a time no less (that is, >=) than the start time.

  auto start_iterator = std::lower_bound(
    data_set_tree_root_nodes.begin(),
    data_set_tree_root_nodes.end(),
    start,
    [](pappso::MsRunDataSetTreeNode *p, double d) {
      return p->getQualifiedMassSpectrum()->getRtInMinutes() < d;
    });

  if(start_iterator == data_set_tree_root_nodes.end())
    {
      qDebug() << "No single mass spectrum was acquired with a rt time "
                  "greater or equal to"
               << start;

      // Return the .end() .end() iterator pairs.
      return std::pair<VectorConstIterator, VectorConstIterator>(
        data_set_tree_root_nodes.end(), data_set_tree_root_nodes.end());
    }

  qDebug() << "Start iterator points to a spectrum acquired at rt time:"
           << (*start_iterator)->getQualifiedMassSpectrum()->getRtInMinutes();

  // Likewise, locate in the tree node, the node that has the mass spectrum
  // acquired at a time no greater (that is, <=) than the end time.

  auto end_iterator = std::upper_bound(
    start_iterator,
    data_set_tree_root_nodes.end(),
    end,
    [](double d, pappso::MsRunDataSetTreeNode *p) {
      return p->getQualifiedMassSpectrum()->getRtInMinutes() > d;
    });
  if(end_iterator == data_set_tree_root_nodes.end())
    {
      qDebug() << "The end iterator is the root nodes vector .end()";
    }
  else
    qDebug() << "End iterator points to a spectrum acquired at rt time:"
             << (*end_iterator)->getQualifiedMassSpectrum()->getRtInMinutes();

  return std::pair<VectorConstIterator, VectorConstIterator>(start_iterator,
                                                             end_iterator);
}


#if 0

// This code was never finished. However I discovered that there was a better
// solution to the fact that we did not want to traverse the whole ms run data
// set tree to simply integrate *one* mass spectrum.

std::size_t
MsRunDataSet::addQualifiedMassSpectraInsideDtRtRange(
  double start,
  double end,
  std::size_t ms_level,
  std::vector<pappso::QualifiedMassSpectrumCstSPtr> &qual_mass_spectra,
  pappso::DataKind data_kind) const
{
  // What we want is add to the qual mass spectra vector, all the mass spectra
  // that were acquired in the start:end range. That start:end range refers to
  // either RT or DT depending on the data kind paramater.

  // Determine once for all the right map depending on the data kind.

  MapDoubleQualMassSpectra map;

  if(data_kind == pappso::DataKind::rt)
    map = m_rtQualMassSpectraMap;
  else if(data_kind == pappso::DataKind::dt)
    map = m_dtQualMassSpectraMap;

  std::size_t added_mass_spectra = 0;

  // Search for all the values in the map that are inside the rt_start:ret_end
  // range.

  using MapIterator = MapDoubleQualMassSpectra::iterator;

  // What is the map item that is not less than rt_start ?

  MapIterator first_iter = map.lower_bound(start);

  if(first_iter == map.end())
    return 0;

  // What is the map item that is not greater than rt_end ?

  MapIterator last_iter = map.upper_bound(end);

  // Now iterate between the two iterators.

  for(MapIterator iter = first_iter; iter != last_iter; ++iter)
    {
      // Each map rt key is associated with a vector of qualmassspectra. We need
      // to copy the pointers to the vector passed as parameter.

      std::vector<pappso::QualifiedMassSpectrumCstSPtr> cur_vector =
        iter->second;

      for(auto mass_spectrum_csp : cur_vector)
        {
          // But we only do this if the mass spectrum was acquired at the right
          // MS level.
          if(mass_spectrum_csp->getMsLevel() == ms_level)
            qual_mass_spectra.push_back(mass_spectrum_csp);
          ++added_mass_spectra;
        }
    }

  return added_mass_spectra;
}


std::size_t
MsRunDataSet::removeQualifiedMassSpectraOutsideDtRtRange(
  double start,
  double end,
  std::vector<pappso::QualifiedMassSpectrumCstSPtr> &qual_mass_spectra,
  pappso::DataKind data_kind) const
{

  // Determine once for all the right map depending on the data kind.

  MapDoubleQualMassSpectra map;

  if(data_kind == pappso::DataKind::rt)
    map = m_rtQualMassSpectraMap;
  else if(data_kind == pappso::DataKind::dt)
    map = m_dtQualMassSpectraMap;

  std::size_t removed_mass_spectra = 0;

  // We want to remove from the vector of mass spectra all the spectra that are
  // outside of the [start:end] range.

  auto will_be_iterator_to_end = std::remove_if(
    qual_mass_spectra.begin(),
    qual_mass_spectra.end(),
    [start, end, data_kind](
      pappso::QualifiedMassSpectrumCstSPtr &mass_spectrum_csp) {
      if(data_kind == pappso::DataKind::rt)
        return (mass_spectrum_csp->getRtInSeconds() < start ||
                mass_spectrum_csp->getRtInSeconds() > end);
      else if(data_kind == pappso::DataKind::rt)
        return (mass_spectrum_csp->getDtInMilliSeconds() < start ||
                mass_spectrum_csp->getDtInMilliSeconds() > end);
      else
        qFatal("Programming error.");
    });

  // At this point, all the mass spectra grouped at will_be_iterator_to_end to
  // the vector end are the ones we need to erase.

  // Compute the number of spectra that will be removed.

  removed_mass_spectra =
    std::distance(will_be_iterator_to_end, qual_mass_spectra.end());

  qual_mass_spectra.erase(will_be_iterator_to_end, qual_mass_spectra.end());

  return removed_mass_spectra;
}

#endif


} // namespace minexpert
} // namespace msxps
