/* 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 <cmath>
#include <algorithm>
#include <limits> // for std::numeric_limits


/////////////////////// Qt includes


/////////////////////// pappsomspp includes
#include <pappsomspp/core/massspectrum/massspectrum.h>
#include <pappsomspp/core/processing/combiners/massspectrumpluscombiner.h>
#include <pappsomspp/core/processing/combiners/tracepluscombiner.h>
#include <pappsomspp/core/trace/trace.h>
#include <pappsomspp/core/processing/filters/filternormalizeintensities.h>
#include <pappsomspp/core/utils.h>


/////////////////////// Local includes
#include "MsXpS/libXpertMassCore/globals.hpp"
#include "MsXpS/libXpertMassCore/Utils.hpp"
#include "MsXpS/libXpertMassCore/MassDataCborMassSpectrumHandler.hpp"
#include "MsXpS/libXpertMassCore/IsotopicClusterShaper.hpp"
#include "MsXpS/libXpertMassCore/MassPeakShaper.hpp"

namespace MsXpS
{

namespace libXpertMassCore
{


/*!
\class MsXpS::libXpertMassCore::IsotopicClusterShaper
\inmodule libXpertMassCore
\ingroup XpertMassCoreMassCalculations
\inheaderfile IsotopicClusterShaper.hpp

\brief The IsotopicClusterShaper class provides the features needed to shape
sets of (peak centroid m/z, intensity) pairs associated to a given charge
into a mass spectral pappso;:Trace.

Each set of (peak centroid m/z, intensity) pairs corresponds to an isotopic
cluster that is associated to a charge.

The configuration of the peak shaping process is held in a specific \l
MassPeakShaperConfig class.

The output of the computation is a pappso::Trace obtained by combining all the
different shapes obtained for the different peak centroids of all the sets of
(peak centroid m/z, intensity) pairs. If binning was requested, the obtained
Trace is the result of a combination accounting for the required bin size,
otherwise the obtained Trace is the result of the mere addition of all the
points in the different traces.

\sa MassPeakShaperConfig
*/

/*!
\variable \
MsXpS::libXpertMassCore::IsotopicClusterShaper::m_isotopicClusterChargePairs

\brief Vector of pappso::Trace instances in pair with charges.
*/

/*!
\variable
MsXpS::libXpertMassCore::IsotopicClusterShaper::mp_massPeakShaperConfig

\brief The configuration driving the mass peak shaping process.
*/

/*!
\variable MsXpS::libXpertMassCore::IsotopicClusterShaper::m_mapTrace

\brief The map relating a m/z value to its intensity.

This map is a variant of pappso::Trace that is designed to allow for easy mass
spectrum combination. It is generally used only for computations and is
converted to a pappso::Trace once all the computations have been carried out.
*/

/*!
\variable MsXpS::libXpertMassCore::IsotopicClusterShaper::m_finalTrace

\brief The pappso::Trace holding the final results of the computations.
*/

/*!
\variable MsXpS::libXpertMassCore::IsotopicClusterShaper::m_mzIntegrationParams

\brief The configuration of the mass spectral combinations (for
example, determines the bins, if binning is required).
*/

/*!
\variable MsXpS::libXpertMassCore::IsotopicClusterShaper::m_mostIntensePeakMz

\brief The most intense peak encountered during the calculations.
*/

/*!
\variable MsXpS::libXpertMassCore::IsotopicClusterShaper::m_smallestMz

\brief The smallest m/z value encountered during the calculations.

This value is required for the crafting of the bins.
*/

/*!
\variable MsXpS::libXpertMassCore::IsotopicClusterShaper::m_greatestMz

\brief The greatest m/z value encountered during the calculations.

This value is required for the crafting of the bins.
*/

/*!
\variable MsXpS::libXpertMassCore::IsotopicClusterShaper::m_normalizeIntensity

\brief The value by which all the peak shapes need to be normalized.
*/


/*!
\brief Constructs a IsotopicClusterShaper instance.

\list
\li \a isotopic_cluster: The peak centroids belonging to an isotopic cluster
in the form of a pappso::Trace.
\li \a charge: The charge of the analyte below the isotopic cluster peaks.
\li \a config: The mass peak shaping process configuration.
\endlist

Each pappso::DataPoint in the \a isotopic_cluster pappso::Trace corresponds to
a (m/z, intensity) peak centroid belonging to the isotopic cluster.
*/
IsotopicClusterShaper::IsotopicClusterShaper(
  const pappso::Trace &isotopic_cluster,
  int charge,
  const MassPeakShaperConfig &config)
  : mp_massPeakShaperConfig(new MassPeakShaperConfig(config))
{
  // No need to reset, we are constructing.
  setIsotopicCluster(isotopic_cluster, charge, false);
}

/*!
\brief Constructs a IsotopicClusterShaper instance.

\list
\li \a isotopic_cluster_charge_pairs: The pairs associating a pappso::Trace
instance to its corresponding charge.
\li \a config: The mass peak shaping process configuration.
\endlist

In this constructor, a set of sets of (m/z, charge) peak centroids is passed as
argument. The pappso::Trace instances in \a isotopic_cluster_charge_pairs might
correspond to all the isotopic clusters representing a given analyte at
different charges.
*/
IsotopicClusterShaper::IsotopicClusterShaper(
  const std::vector<IsotopicClusterChargePair> &isotopic_cluster_charge_pairs,
  const MassPeakShaperConfig &config)
  : mp_massPeakShaperConfig(new MassPeakShaperConfig(config))
{
  // No need to reset, we are constructing.
  setIsotopicClusterChargePairs(isotopic_cluster_charge_pairs, false);
}

/*!
\brief Destructs this IsotopicClusterShaper instance.
*/
IsotopicClusterShaper::~IsotopicClusterShaper()
{
}

/*!
\brief Sets the peak shaping process \a{config}uration.
*/
void
IsotopicClusterShaper::setConfig(const MassPeakShaperConfig &config)
{
  mp_massPeakShaperConfig->initialize(config);
}

/*!
\brief Gets the peak shaping process configuration.
*/
const MassPeakShaperConfig *
IsotopicClusterShaper::getConfig() const
{
  return mp_massPeakShaperConfig;
}

/*!
\brief Sets the intensity normalization value to \a max_intensity.
*/
void
IsotopicClusterShaper::setNormalizeIntensity(int max_intensity)
{
  m_normalizeIntensity = max_intensity;
}

/*!
\brief Gets the intensity normalization value.
*/
int
IsotopicClusterShaper::getNormalizeIntensity() const
{
  return m_normalizeIntensity;
}

void
IsotopicClusterShaper::forciblySetSmallestMz(double mz)
{
  m_smallestMz = mz;
}

double
IsotopicClusterShaper::getSmallestMz() const
{
  return m_smallestMz;
}

void
IsotopicClusterShaper::forciblySetGreatesttMz(double mz)
{
  m_greatestMz = mz;
}

double
IsotopicClusterShaper::getGreatestMz() const
{
  return m_greatestMz;
}

void
IsotopicClusterShaper::forciblysetMzRange(double start_mz, double stop_mz)
{
  forciblySetSmallestMz(start_mz);
  forciblySetGreatesttMz(stop_mz);
}


/*!
\brief Runs the mass peak shaping process for all the \l
IsotopicClusterChargePair objects in \l m_isotopicClusterChargePairs.

If \a reset is true, the member \l m_mapTrace and \l m_finalTrace are cleared
before starting the computations. The \l m_config member is first
\l{MassPeakShaperConfig::resolve()}d to check that all the parameters have been
properly set and are valid.

Returns the obtained pappso::Trace corresponding to the combination of all the
individual traces obtained for the various isotopic clusters and their
corresponding charge.
*/
pappso::Trace &
IsotopicClusterShaper::run(bool reset)
{
  if(reset)
    {
      // Clear the map trace that will receive the results of the combinations.
      m_mapTrace.clear();
      m_finalTrace.clear();
    }

  ErrorList error_list;

  if(!mp_massPeakShaperConfig->resolve(error_list))
    {
      qDebug() << "Failed to resolve the MassPeakShaperConfig with errors:\n"
               << Utils::joinErrorList(error_list, "\n");
      return m_finalTrace;
    }

  // This class works on a vector of pairs containing the following:
  // 1. a pappso::TraceCstSPtr
  // 2. a charge

  // We will process each pair in turn. If the integration requires bin, then
  // each shaped isotopic cluster will be combined into a mass spectrum.
  // Otherwise a trace combiner will be used.

  // When setting the data (either by construction or using the set<> functions,
  // we had monitored the smallest and the greatest m/z value over the whole set
  // of the DataPoint objects in the isotopic clusters (centroid data). This is
  // because we need, in case binning is required, these values to craft the
  // bins.

  // We will need to perform combinations, positive combinations.
  // This mass spectrum combiner is in case we need binning.
  pappso::MassSpectrumPlusCombiner mass_spectrum_plus_combiner;

  // This trace combiner is in case do *not* need binning.
  pappso::TracePlusCombiner trace_plus_combiner(-1);

  // Configure the mass spectrum combiner in case we need binning.

  if(mp_massPeakShaperConfig->isWithBins())
    {
      // Bins were requested.

      // qDebug() << "Bins are required.";

      // Get the bin size out of the configuration.

      double bin_size = mp_massPeakShaperConfig->getBinSize();

      // qDebug() << "The bin size in the config is:" << bin_size;

      // Because we had monitored the m/z values of all the shapes generated
      // above, we know the smallest and greatest m/z value that were
      // encountered in all that peak shaping activity. We thus can create the
      // bins faithfully.

      Enums::MassPeakWidthLogic logic =
        mp_massPeakShaperConfig->getMassPeakWidthLogic();

      // The m_smallestMz and m_greatestMz values have been determined by
      // looking into the unshaped isotopic clusters passed to this object
      // either by construction or with functions. These two mz values are thus
      // peak centroids, not data points belonging to a shaped peak since we
      // have not yet started shaping the peaks. This means that we cannot
      // create bins start / ending at these values because we would loose the
      // first half of the first shaped peak centroid and the second half of the
      // last shaped peak centroid (a shaped peak goes left *and* right of the
      // peak centroid otherwise there would be no shape).
      //
      // This is why we provide a confortable margin fo the bin creation below
      // by removing 1 Th on the left of the smallest mz and by adding 1 Th on
      // right of the greatest mz.

      if(logic == Enums::MassPeakWidthLogic::FWHM)
        {
          m_mzIntegrationParams.initialize(
            m_smallestMz - 1,
            m_greatestMz + 1,
            pappso::MzIntegrationParams::BinningType::ARBITRARY,
            pappso::PrecisionFactory::getDaltonInstance(bin_size),
            /*binSizeDivisor*/ 1,
            /*decimalPlacesr*/ -1,
            true,
            nullptr);
        }
      else if(logic == Enums::MassPeakWidthLogic::RESOLUTION)
        {
          double resolution = mp_massPeakShaperConfig->getResolution();

          m_mzIntegrationParams.initialize(
            m_smallestMz - 1,
            m_greatestMz + 1,
            pappso::MzIntegrationParams::BinningType::ARBITRARY,
            pappso::PrecisionFactory::getResInstance(resolution),
            mp_massPeakShaperConfig->getBinSizeDivisor(),
            -1,
            true,
            nullptr);
        }
      else
        qFatal(
          "Programming error. By this time, the mass peak width logic should "
          "have been defined.");

      // qDebug() << "The mz integration params:"
      //<< m_mzIntegrationParams.toString();

      // Now compute the bins.

      std::vector<double> bins = m_mzIntegrationParams.createBins();
      // qDebug() << "The bins:" << bins;

      mass_spectrum_plus_combiner.setBins(bins);
      // qDebug() << "Set bins to the mass spectrum combiner:"
      //<< mass_spectrum_plus_combiner.getBins().size();
    }

  std::size_t peak_centroid_count    = 0;
  std::size_t isotopic_cluster_count = 0;

  // Iterate in the isotopic cluster/charge pairs.
  for(auto isotopic_cluster_charge_pair : m_isotopicClusterChargePairs)
    {
      int charge = isotopic_cluster_charge_pair.second;

      // Iterate in the data points of the current centroid data isotopic
      // cluster.
      for(auto data_point : *isotopic_cluster_charge_pair.first)
        {
          qDebug() << "Now computing shape for DataPoint:"
                   << data_point.toString();

          // Note the division by m_charge below!

          pappso::Trace trace = MassPeakShaper::computePeakShape(
            data_point.x / charge, data_point.y, *mp_massPeakShaperConfig);

          qDebug().noquote()
            << "The shaped isotopic cluster has size:" << trace.size()
            << "with DataPoint content:" << trace.toString();

          if(trace.size())
            {
              if(mp_massPeakShaperConfig->isWithBins())
                mass_spectrum_plus_combiner.combine(m_mapTrace, trace);
              else
                trace_plus_combiner.combine(m_mapTrace, trace);

              // qDebug() << qSetRealNumberPrecision(15)
              //<< "The map trace for combination has size:"
              //<< m_mapTrace.size()
              //<< "and starting m/z:" << m_mapTrace.begin()->first
              //<< "and ending m/z:"
              //<< std::prev(m_mapTrace.end())->first;

              ++peak_centroid_count;
            }
        }
      ++isotopic_cluster_count;
    }

  // qDebug() << QString(
  //"Successfully processed %1 isotopic clusters for a total of %2 "
  //"shaped peak centroids")
  //.arg(isotopic_cluster_count)
  //.arg(peak_centroid_count);

  // The user might have asked that the most intense m/z peak centroid be used
  // for normalization. In that case that peak centroid's intensity is brought
  // to m_normalizeIntensity and the ratio between its current intensity and
  // m_normalizeIntensity is used to normalize all the other data points in the
  // trace.

  if(m_normalizeIntensity != 1)
    {

      // qDebug() << "Now normalizing to  intensity = " << m_normalizeIntensity;

      pappso::Trace trace = m_mapTrace.toTrace();
      m_finalTrace =
        trace.filter(pappso::FilterNormalizeIntensities(m_normalizeIntensity));

      // double max_int = normalized_trace.maxYDataPoint().y;
      // qDebug() << "After normalization max int:" << max_int;
    }
  else
    m_finalTrace = m_mapTrace.toTrace();

  // qDebug() << "Returning a trace of size:" << m_finalTrace.size();

  // pappso::Utils::writeToFile(m_finalTrace.toString(), "/tmp/mass/trace.txt");

  return m_finalTrace;
}

/*!
\brief Handles the \a isotopic_cluster_sp input isotopic cluster as a
pappso::Trace.

\a isotopic_cluster_sp is associated to a \a charge. If \a reset is true, the
member vector of \l IsotopicClusterChargePair instances is cleared before the
computations.

This function is the workhorse for all the functions used to set the initial
data for the computations. Its main task is to scrutinize the data in \a
isotopic_cluster_sp and update the \l m_smallestMz, \l m_greatestMz and \l
m_mostIntensePeakMz values based on the data passed as argument.
*/
void
IsotopicClusterShaper::setIsotopicCluster(
  pappso::TraceCstSPtr isotopic_cluster_sp, int charge, bool reset)
{
  if(reset)
    m_isotopicClusterChargePairs.clear();

  double min_x = isotopic_cluster_sp->minX();
  m_smallestMz = std::min(m_smallestMz, min_x);

  double max_x = isotopic_cluster_sp->maxX();
  m_greatestMz = std::max(m_greatestMz, max_x);

  m_mostIntensePeakMz = isotopic_cluster_sp->maxYDataPoint().x;

  // qDebug() << qSetRealNumberPrecision(15) << "m_smallestMz:" << m_smallestMz
  //<< "m_greatestMz:" << m_greatestMz
  //<< "m_mostIntensePeakMz:" << m_mostIntensePeakMz;

  m_isotopicClusterChargePairs.push_back(
    IsotopicClusterChargePair(isotopic_cluster_sp, charge));

  mp_massPeakShaperConfig->setReferencePeakMz(m_mostIntensePeakMz);
}

/*!
\brief Handles the \a isotopic_cluster_sp input isotopic cluster as a
pappso::Trace.

\a isotopic_cluster_sp is associated to a \a charge.

The member vector of \l IsotopicClusterChargePair instances is cleared before
the computations.
*/
void
IsotopicClusterShaper::setIsotopicCluster(
  pappso::TraceCstSPtr isotopic_cluster_sp, int charge)
{
  setIsotopicCluster(isotopic_cluster_sp, charge, true);
}

/*!
\brief Handles the \a isotopic_cluster input isotopic cluster as a
pappso::Trace.

\a isotopic_cluster is associated to a \a charge. If \a reset is true, the
member vector of \l IsotopicClusterChargePair instances is cleared before the
computations.
*/
void
IsotopicClusterShaper::setIsotopicCluster(const pappso::Trace &isotopic_cluster,
                                          int charge,
                                          bool reset)
{
  setIsotopicCluster(
    std::make_shared<const pappso::Trace>(isotopic_cluster), charge, reset);
}

/*!
\brief Handles the \a isotopic_cluster input isotopic cluster as a
pappso::Trace.

\a isotopic_cluster is associated to a \a charge.

The member vector of \l IsotopicClusterChargePair instances is cleared before
the computations.
*/
void
IsotopicClusterShaper::setIsotopicCluster(const pappso::Trace &isotopic_cluster,
                                          int charge)
{
  setIsotopicCluster(
    std::make_shared<const pappso::Trace>(isotopic_cluster), charge, true);
}

/*!
\brief Handles the \a isotopic_cluster_charge_pair input isotopic cluster as a
IsotopicClusterChargePair.

The member vector of \l IsotopicClusterChargePair instances is cleared before
the computations.
*/
void
IsotopicClusterShaper::setIsotopicCluster(
  IsotopicClusterChargePair isotopic_cluster_charge_pair)
{
  setIsotopicCluster(isotopic_cluster_charge_pair.first,
                     isotopic_cluster_charge_pair.second,
                     true);
}

/*!
\brief Handles the \a isotopic_cluster_charge_pair input isotopic cluster as a
IsotopicClusterChargePair.

If \a reset is true, the member vector of \l IsotopicClusterChargePair instances
is cleared before the computations.
*/
void
IsotopicClusterShaper::setIsotopicCluster(
  IsotopicClusterChargePair isotopic_cluster_charge_pair, bool reset)
{
  setIsotopicCluster(isotopic_cluster_charge_pair.first,
                     isotopic_cluster_charge_pair.second,
                     reset);
}

/*!
\brief Handles the \a isotopic_cluster_charge_pairs input isotopic cluster as a
vector of IsotopicClusterChargePair instances.

If \a reset is true, the member vector of \l IsotopicClusterChargePair
instances is cleared before the computations.
*/
void
IsotopicClusterShaper::setIsotopicClusterChargePairs(
  const std::vector<IsotopicClusterChargePair> &isotopic_cluster_charge_pairs,
  bool reset)
{
  for(auto cluster_charge_pair : isotopic_cluster_charge_pairs)
    setIsotopicCluster(
      cluster_charge_pair.first, cluster_charge_pair.second, reset);

  // qDebug() << qSetRealNumberPrecision(15) << "m_smallestMz:" << m_smallestMz
  //<< "m_greatestMz:" << m_greatestMz
  //<< "m_mostIntensePeakMz:" << m_mostIntensePeakMz;
}

/*!
\brief Handles the \a isotopic_cluster_charge_pairs input isotopic cluster as a
vector of IsotopicClusterChargePair instances.

The member vector of \l IsotopicClusterChargePair instances is cleared before
the computations.
*/
void
IsotopicClusterShaper::setIsotopicClusterChargePairs(
  const std::vector<IsotopicClusterChargePair> &isotopic_cluster_charge_pairs)
{
  setIsotopicClusterChargePairs(isotopic_cluster_charge_pairs, true);

  // qDebug() << qSetRealNumberPrecision(15) << "m_smallestMz:" << m_smallestMz
  //<< "m_greatestMz:" << m_greatestMz
  //<< "m_mostIntensePeakMz:" << m_mostIntensePeakMz;
}

/*!
\brief Adds an isotopic cluster in the form of the \a isotopic_cluster
pappso::Trace associated to the corresponding \a charge.

The member vector of \l IsotopicClusterChargePair instances is \e{not} cleared
before the computations.
*/
void
IsotopicClusterShaper::appendIsotopicCluster(
  const pappso::Trace &isotopic_cluster, int charge)
{
  // Do not clear the isotopic clusters!

  setIsotopicCluster(isotopic_cluster, charge, false);
}

/*!
\brief Adds  isotopic clusters in the form of the \a
isotopic_cluster_charge_pairs vector of IsotopicClusterChargePair instances.

The member vector of \l IsotopicClusterChargePair instances is \e{not} cleared
before the computations.
*/
void
IsotopicClusterShaper::appendIsotopicClusterChargePairs(
  const std::vector<IsotopicClusterChargePair> &isotopic_cluster_charge_pairs)
{
  setIsotopicClusterChargePairs(isotopic_cluster_charge_pairs, false);

  // qDebug() << "m_smallestMz:" << m_smallestMz << "m_greatestMz:" <<
  // m_greatestMz
  //<< "m_mostIntensePeakMz:" << m_mostIntensePeakMz;
}

/*!
\brief Returns the final result of all the computations as a string.
*/
QString
IsotopicClusterShaper::shapeToString()
{
  return m_finalTrace.toString();
}

} // namespace libXpertMassCore

} // namespace MsXpS
