/* 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 <QObject>
#include <QAtomicInt>
#include <QSharedPointer>
#include <QElapsedTimer>
#include <QThreadPool>
#include <QtConcurrent/QtConcurrent>

/////////////////////// pappsomspp includes


/////////////////////// libXpertMassCore includes
#include "MsXpS/libXpertMassCore/IsotopicClusterShaper.hpp"


/////////////////////// libXpertMassGui includes
#include <MsXpS/libXpertMassCore/MassSpectrumSynthesizer.hpp>

/////////////////////// Local includes

namespace MsXpS
{
namespace libXpertMassCore
{


MassSpectrumSynthesizer::MassSpectrumSynthesizer(Data data, QObject *parent)
  : MonitoredTaskInterface(parent)
{
  m_data.initialize(data);
}

MassSpectrumSynthesizer::~MassSpectrumSynthesizer()
{
  // First destroy the stream!
  if(mp_logTextStream != nullptr)
    {
      // I think the flush is performed automatically upon destruction of the
      // stream, but anyways.
      mp_logTextStream->flush();
      delete mp_logTextStream;
    }

  // Next destroy the file!
  if(mp_logFile != nullptr)
    delete mp_logFile;
}

void
MassSpectrumSynthesizer::setData(const Data &data)
{
  m_data.initialize(data);
}

void
MassSpectrumSynthesizer::setUserData(void *user_data_vp)
{
  mvp_userData = user_data_vp;
}

void
MassSpectrumSynthesizer::setIsotopicClusterChargePairs(
  std::vector<libXpertMassCore::IsotopicClusterChargePair> &pairs)
{
  m_isotopicClusterChargePairs = pairs;
}

void
MassSpectrumSynthesizer::setIsotopicClusterChargePairs(
  std::vector<libXpertMassCore::IsotopicClusterChargePair> &&pairs)
{
  m_isotopicClusterChargePairs = std::move(pairs);
}

void
MassSpectrumSynthesizer::generateIsotopicClusters()
{
  QString message = "Start synthesizing mass spectrum";
  // QString processing_details_string = message + "\n";
  qDebug() << message;

  if(m_data.isotopicDataSPtr == nullptr)
    qFatal() << "Programming error. The pointer cannot be nullptr.";

  message = QString("Using isotope definitions from %1")
              .arg(m_data.isotopicDataSPtr->getFilePath());
  qDebug() << message;

  // We do not have values of progress bar limits.
  emit taskSetMinMaxValuesSignal(0, m_data.formula_charge_pairs.size());

  // And now we can instantiate a cluster generator

  IsotopicClusterGenerator isotopic_cluster_generator(m_data.isotopicDataSPtr);
  isotopic_cluster_generator.setFormulaChargePairs(m_data.formula_charge_pairs);
  isotopic_cluster_generator.setMaxSummedProbability(
    m_data.cumulatedProbabilities);
  isotopic_cluster_generator.setNormalizationIntensity(
    m_data.normalizationIntensity);
  isotopic_cluster_generator.setIsotopicDataType(IsotopicDataType::USER_CONFIG);

  connect(&isotopic_cluster_generator,
          &IsotopicClusterGenerator::processingCounterSignal,
          this,
          [&](int count) {
            QString status_message =
              QString("Computing isotopic cluster %1").arg(count);
            emit taskSetStatusMessageAndCurrentValueSignal(status_message,
                                                           count);
          });


  // And now perform the work. For each formula/charge pair, the generator will
  // create an isotopic cluster shaped trace associated to the corresponding
  // charge in a libXpertMassCore::IsotopicClusterChargePair. All these pairs
  // are stored in a vector.

  int count = isotopic_cluster_generator.run();

  if(!count)
    {
      emit taskSetStatusMessageSignal("Failed to create any isotopic cluster");
      m_data.errorList->append("Failed to create any isotopic cluster");
      emit taskErrorSignal(m_data.errorList);
      emit taskFinishedSignal();
      emit finishedSignal(pappso::Trace(),
                          m_data.massSpectrumTitle,
                          m_data.massSpectrumColorName,
                          mvp_userData);

      return;
    }

  if(m_cancelledTask)
    {
      emit taskCancelledSignal();
      emit finishedSignal(pappso::Trace(),
                          m_data.massSpectrumTitle,
                          m_data.massSpectrumColorName,
                          mvp_userData);
    }

  message = QString("Generated %1 isotopic clusters").arg(count);
  emit taskSetStatusMessageSignal(message);
  qDebug() << message;

  m_isotopicClusterChargePairs.clear();

  m_isotopicClusterChargePairs =
    isotopic_cluster_generator.getIsotopicClusterChargePairs();
}

void
MassSpectrumSynthesizer::shapeIsotopicClusters()
{
  QString message;

  // At this point we should use these pairs to create a shape for each. But
  // first reset to 1 the charge because that charge was already accounted for
  // at the generation of the cluster. We do not want to divide m/z again by
  // charge. If charge had been 1, that would be no problem, but let's say the
  // charge was 2, if we did maintain that charge to a value of 2, then we would
  // get a tetra-protonated species cluster.

  // This task may be lengthy if resolving power is very high and the analyte is
  // a large molecule, because the number of centroids in the isotopic cluster
  // will be very large (in particular is summed probabilities is > 0.99). For
  // this reason, we need to provide feedback to the user with the proper
  // granularity, which is the count of processed isotopic cluster centroids
  // (that is, each time a new centroid is shaped).

  emit taskSetDescriptionTextSignal(
    "Now shaping the isotopic clusters' centroids");
  int isotopic_clusters_total_centroid_count = 0;

  // Note how we ask a reference to the pair that is iterated into, otherwise we
  // would get a copy and we would lose the local charge modification.
  for(libXpertMassCore::IsotopicClusterChargePair &pair :
      m_isotopicClusterChargePairs)
    {
      pair.second = 1;

      isotopic_clusters_total_centroid_count += pair.first->size();
    }

  emit taskSetMinMaxValuesSignal(0, isotopic_clusters_total_centroid_count);


  // Now instantiate the isotopic cluster shaper and set the clusters' data in
  // it.

  libXpertMassCore::IsotopicClusterShaper isotopic_cluster_shaper(
    m_isotopicClusterChargePairs, m_data.massPeakShaperConfig);

  isotopic_cluster_shaper.setNormalizeIntensity(m_data.normalizationIntensity);

  // In the caller environment, the user might have set a fixed m/z
  // range for the final mass spectrum because this way even if the
  // singular mass spectra have differing final m/z ranges, the bins
  // of the final combination mass spectrum will be computed for that
  // fixed m/z range, thus having a proper "phasing" of all the sub-spectra.

  if(m_data.fixedMzRangeStop)
    isotopic_cluster_shaper.forciblySetMzRange(m_data.fixedMzRangeStart,
                                               m_data.fixedMzRangeStop);

  connect(&isotopic_cluster_shaper,
          &IsotopicClusterShaper::processingCounterSignal,
          this,
          [&](int count) {
            QString status_message =
              QString("Shaping isotopic cluster %1").arg(count);
            emit taskSetStatusMessageAndCurrentValueSignal(status_message,
                                                           count);
            qDebug() << status_message;
            
          });

  // And now run the shaper.

  qDebug() << "Now running the isotopic cluster";

  pappso::Trace mass_spectrum = isotopic_cluster_shaper.run();
  int mass_spectrum_size      = mass_spectrum.size();

  if(!mass_spectrum_size)
    {
      qDebug() << "Synthesized empty mass spectrum";

      m_data.errorList->append("Synthesized empty mass spectrum");
      emit taskErrorSignal(m_data.errorList);
      qDebug() << "Synthesized empty mass spectrum";
      emit taskFinishedSignal();
      emit finishedSignal(pappso::Trace(),
                          m_data.massSpectrumTitle,
                          m_data.massSpectrumColorName,
                          mvp_userData);
      return;
    }

  message = QString("Synthesized mass spectrum (%1 data points)")
              .arg(mass_spectrum_size);

  emit taskSetStatusMessageSignal(message);

  qDebug() << message;

  // This signal will trigger the end of the task monitor composite widget.
  emit taskFinishedSignal();

  // This signal tells the thread has finished doing its computation
  emit finishedSignal(mass_spectrum,
                      m_data.massSpectrumTitle,
                      m_data.massSpectrumColorName,
                      mvp_userData);
}

// This class can be used to perform either the full sequence:
// formula,charge -> isotopic cluster,charge -> shaped clusters -> mass spectrum
// or only the second part, if clusters are already available:
// isotopic cluster,charge -> shaped clusters -> mass spectrum
void
MassSpectrumSynthesizer::run()
{
  if(m_isRunning)
    {
      qWarning() << "Matcher already running. Ignoring duplicate start.";
      return;
    }

  if(m_isotopicClusterChargePairs.size())
    {
      return shapeIsotopicClusters();
    }

  generateIsotopicClusters();
  shapeIsotopicClusters();
}

void
MassSpectrumSynthesizer::cancelTask()

{
  m_cancelledTask = true;
}

} // namespace libXpertMassCore
} // namespace MsXpS
