/* 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 <iostream>
#include <iomanip>

/////////////////////// OpenMP include
#include <omp.h>

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

/////////////////////// pappsomspp includes
#include <pappsomspp/processing/combiners/tracepluscombiner.h>


/////////////////////// Local includes
#include "QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz.hpp"
#include "ProcessingSpec.hpp"
#include "ProcessingStep.hpp"
#include "ProcessingType.hpp"


int qualifiedMassSpectrumVectorMassDataIntegratorToDtRtMzMetaTypeId =
  qRegisterMetaType<
    msxps::minexpert::QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz>(
    "msxps::minexpert::QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz");

int qualifiedMassSpectrumVectorMassDataIntegratorToDtRtMzSPtrMetaTypeId =
  qRegisterMetaType<
    msxps::minexpert::
      QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMzSPtr>(
    "msxps::minexpert::"
    "QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMzSPtr");


namespace msxps
{
namespace minexpert
{


// Needed for qRegisterMetaType
QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz::
  QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz()
  : QualifiedMassSpectrumVectorMassDataIntegrator()
{
}


QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz::
  QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz(
    MsRunDataSetCstSPtr ms_run_data_set_csp,
    const ProcessingFlow &processing_flow,
    QualifiedMassSpectraVectorSPtr &qualified_mass_spectra_to_integrate_sp)
  : QualifiedMassSpectrumVectorMassDataIntegrator(
      ms_run_data_set_csp,
      processing_flow,
      qualified_mass_spectra_to_integrate_sp)
{
  m_processingFlow.setMsRunDataSetCstSPtr(mcsp_msRunDataSet);
}


// Needed for qRegisterMetaType
QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz::
  QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz(
    const QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz &other)
  : QualifiedMassSpectrumVectorMassDataIntegrator(other)
{
}


QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz::
  ~QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz()
{
}


std::size_t
QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz::getColorMapKeyCellCount()
  const
{
  return m_colorMapKeyCellCount;
}


std::size_t
QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz::getColorMapMzCellCount()
  const
{
  return m_colorMapMzCellCount;
}


double
QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz::getColorMapMinKey() const
{
  return m_colorMapMinKey;
}


double
QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz::getColorMapMaxKey() const
{
  return m_colorMapMaxKey;
}


double
QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz::getColorMapMinMz() const
{
  return m_colorMapMinMz;
}


double
QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz::getColorMapMaxMz() const
{
  return m_colorMapMaxMz;
}


using Map = std::map<double, pappso::MapTrace>;
const std::shared_ptr<Map> &
QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz::
  getDoubleMapTraceMapSPtr() const
{
  return m_doubleMapTraceMapSPtr;
}

pappso::DataKind
QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz::
  getLastIntegrationDataKind() const
{
  return m_lastIntegrationDataKind;
}


void
QualifiedMassSpectrumVectorMassDataIntegratorToDtRtMz::integrate(
  pappso::DataKind data_kind)
{
  // qDebug() << "Data kind:" << static_cast<int>(data_kind);

  std::chrono::system_clock::time_point chrono_start_time =
    std::chrono::system_clock::now();

  // In this integration, we are going to craft the data to create a colormap
  // that will plot mz data with either rt or dt data (depending on data_kind).

  m_lastIntegrationDataKind = data_kind;

  // If there are no data, nothing to do.
  std::size_t mass_spectra_count =
    mcsp_qualifiedMassSpectraToIntegrateVector->size();

  if(!mass_spectra_count)
    {
      qDebug() << "The qualified mass spectrum vector is empty, nothing to do.";

      emit cancelOperationSignal();
    }

  // Nothing special to do about processing flow info because that must have
  // been set before calling this function.

  // We need to clear the map trace!
  m_mapTrace.clear();

  // Choose the right message to emit and make sanity checks.
  if(m_lastIntegrationDataKind == pappso::DataKind::rt)
    {
      emit setTaskDescriptionTextSignal(
        "Integrating to a TIC chromatogram/mass spectrum color map");
    }
  else if(m_lastIntegrationDataKind == pappso::DataKind::dt)
    {
      emit setTaskDescriptionTextSignal(
        "Integrating to a drift spectrum/mass spectrum color map");
    }
  else
    qFatal("Programming error.");


  // We need a processing flow to work, and, in particular, a processing step
  // from which to find the specifics of the calculation.

  if(!m_processingFlow.size())
    qFatal("The processing flow cannot be empty. Program aborted.");

  // Get the most recent step that holds all the specifics of this integration.
  const ProcessingStep *most_recent_processing_step_p =
    m_processingFlow.mostRecentStep();

  // There must be at least one ProcessingSpec (and thus type) in the
  // processing step since we need to know what color map is being computed
  // (either rt/mz or dt/mz).
  if(!most_recent_processing_step_p->processingTypes().size())
    qFatal("The processing step must have at least one type. Program aborted.");

  // Using a shared pointer make it easy to handle the copying of the map to
  // users of that map without bothering.

  if(m_doubleMapTraceMapSPtr == nullptr)
    m_doubleMapTraceMapSPtr =
      std::make_shared<std::map<double, pappso::MapTrace>>();
  else
    m_doubleMapTraceMapSPtr->clear();

  // In this function, the first key is either dt or rt and the second key does
  // not have use.

  // At this point, allocate a visitor that is specific for the calculation of
  // the mass spectrum. Since we are not binning, we can use a visitor that
  // integrates to a pappso::Trace, not a pappso::MassSpectrum.

  // But we want to parallelize the computation. Se we will allocate a vector of
  // visitors (as many as possible for the available processor threads), each
  // visitor having a subset of the initial data to integrate.

  // Determine the best number of threads to use and how many spectra to provide
  // each of the threads.

  // In the pair below, first is the ideal number of threads and second is the
  // number of mass spectra per thread.
  std::pair<std::size_t, std::size_t> best_parallel_params =
    bestParallelIntegrationParams(
      mcsp_qualifiedMassSpectraToIntegrateVector->size());

  // Handy alias.
  using MassSpecVector = std::vector<pappso::QualifiedMassSpectrumCstSPtr>;
  using VectorIterator = MassSpecVector::const_iterator;

  // Prepare a set of iterators that will distribute all the mass spectra
  // uniformly.
  std::vector<std::pair<VectorIterator, VectorIterator>> iterators =
    calculateIteratorPairs(best_parallel_params.first,
                           best_parallel_params.second);

  // Set aside a vector of std::map<double, pappso::MapTrace> to store retention
  // time values and the combined mass spectrum.

  // Handy aliases.
  using Map     = std::map<double, pappso::MapTrace>;
  using MapSPtr = std::shared_ptr<Map>;

  std::vector<MapSPtr> vector_of_double_maptrace_maps;
  for(std::size_t iter = 0; iter < iterators.size(); ++iter)
    vector_of_double_maptrace_maps.push_back(std::make_shared<Map>());

  // Now that we have all the parallel material, we can start the parallel work.
  emit setProgressBarMaxValueSignal(mass_spectra_count);

  omp_set_num_threads(iterators.size());
#pragma omp parallel for ordered
  for(std::size_t iter = 0; iter < iterators.size(); ++iter)
    {
      // qDebug() << "thread index:" << iter;

      // Get the iterator range that we need to deal in this thread.
      VectorIterator current_iterator = iterators.at(iter).first;
      VectorIterator end_iterator     = iterators.at(iter).second;

      // Get the map for this thread.

      MapSPtr current_double_maptrace_map_sp =
        vector_of_double_maptrace_maps.at(iter);

      // Set the decimal places to 0 below, because we have a large
      // binning for the color map with mz data, otherwize it would
      // be huge.
      pappso::TracePlusCombiner combiner(0);

      // We need to inform the combiner if we have to limit the mz range
      // for the combinations.

      // These values are set in the in the base class constuctor.

      if(m_mzRange.first != std::numeric_limits<double>::max() &&
         m_mzRange.second != std::numeric_limits<double>::min())
        {
          combiner.setFilterResampleKeepXRange(pappso::FilterResampleKeepXRange(
            m_mzRange.first, m_mzRange.second));
        }

      while(current_iterator != end_iterator)
        {
          // We want to be able to intercept any cancellation of the
          // operation.

          if(m_isOperationCancelled)
            break;

          // Make the computation.

          // For each mass spectrum all we need is compute its TIC value, that
          // is, the sum of all its intensities (the y value of the DataPoint
          // instances it contains).

          pappso::QualifiedMassSpectrumCstSPtr qualified_mass_spectrum_csp =
            *current_iterator;

          // Ensure that the qualified mass spectrum contains a mass spectrum
          // that effectively contains the binary (mz,i) data.

          qualified_mass_spectrum_csp =
            checkQualifiedMassSpectrum(qualified_mass_spectrum_csp);

          // Great, at this point we actually have the mass spectrum!

          // We now need to ensure that the processing flow allows for this mass
          // spectrum to be combined.

          // Immediately check if the qualified mass spectrum is of a MS level
          // that matches the greatest MS level found in the member processing
          // flow instance.

          if(!checkMsLevel(qualified_mass_spectrum_csp) ||
             !checkRtRange(qualified_mass_spectrum_csp) ||
             !checkDtRange(qualified_mass_spectrum_csp))
            {
              // if(!checkMsLevel(qualified_mass_spectrum_csp))
              // qDebug() << "MS level did not check.";

              // if(!checkRtRange(qualified_mass_spectrum_csp))
              // qDebug() << "RT range did not check.";

              // if(!checkDtRange(qualified_mass_spectrum_csp))
              // qDebug() << "DT range did not check.";

              // qDebug() << "The checks returned false";

              ++current_iterator;
              continue;
            }

          // 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.

          // qDebug() << "The processing flow has" << m_processingFlow.size() <<
          // "steps.";

          // Boolean value needed because we need to break two loops.
          bool should_increment_iterator_and_continue = false;

          for(auto &&step : m_processingFlow)
            {
              if(!checkProcessingStep(*step, qualified_mass_spectrum_csp))
                {
                  should_increment_iterator_and_continue = true;
                  break;
                }
            }

          if(should_increment_iterator_and_continue)
            {
              ++current_iterator;
              continue;
            }

          // Great, at this point we actually have the mass spectrum and we need
          // to get the dt|rt value for it.

          double first_key; // that is, either rt or dt.

          if(data_kind == pappso::DataKind::rt)
            {
              first_key = qualified_mass_spectrum_csp->getRtInMinutes();
            }
          else if(data_kind == pappso::DataKind::dt)
            {
              first_key = qualified_mass_spectrum_csp->getDtInMilliSeconds();
            }
          else
            qFatal("Programming error.");

          // We need to iterate in the current_map_trace_sp and find if there is
          // already a map trace by the same dt|rt value in it.

          Map::iterator double_maptrace_map_iterator;

          double_maptrace_map_iterator =
            current_double_maptrace_map_sp->find(first_key);

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

              // The map already contained a pair that had the key value ==
              // first_key. All we have to do is combine the new mass
              // spectrum pointed to by the map value into the MapTrace that is
              // pointed to by the map key.

              // Combine the new mass spectrum right into the found MapTrace.

              // qDebug() << "Before combination, map trace map size: "
              //<< double_map_trace_map_iterator->second.size();

              combiner.combine(
                double_maptrace_map_iterator->second,
                *(qualified_mass_spectrum_csp->getMassSpectrumCstSPtr()));
            }
          else
            {

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

              // qDebug() << "No node for a mass spectrum having that same
              // first_key " "was found already:"
              //<< first_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;

              combiner.combine(
                map_trace,
                *(qualified_mass_spectrum_csp->getMassSpectrumCstSPtr()));

              // Finally, store in the map, the map_trace along with its
              // first_key.

              (*current_double_maptrace_map_sp)[first_key] = map_trace;

              // qDebug() << "After combination, map trace map size: " <<
              // map_trace.size();
            }

          // At this point we have processed the qualified mass spectrum.

          emit incrementProgressBarCurrentValueAndSetStatusTextSignal(
            1, "Processing spectra");

          // Now we can go to the next mass spectrum.
          ++current_iterator;
        }
      // End of
      // while(current_iterator != end_iterator)
    }
  // End of
  // #pragma omp parallel for ordered
  // for(std::size_t iter = 0; iter < iterators.size(); ++iter)

  // In the loop above, for each thread, a std::map<double, pappso::MapTrace>
  // map was filled in with the dt|rt, MapTrace pairs.

  // If the task was cancelled, the monitor widget was locked. We need to
  // unlock it.
  emit unlockTaskMonitorCompositeWidgetSignal();
  emit setupProgressBarSignal(0, vector_of_double_maptrace_maps.size() - 1);

  // We might be here because the user cancelled the operation in the for loop
  // above (visiting all the visitors). In this case m_isOperationCancelled is
  // true. We want to set it back to false, so that the following loop is gone
  // through. The user can ask that the operation be cancelled once more. But
  // we want that at least the performed work be used to show the trace.

  m_isOperationCancelled = false;
  int iter               = 0;

  pappso::TracePlusCombiner combiner(0);

  for(auto &&map_sp : vector_of_double_maptrace_maps)
    {
      // qDebug();

      ++iter;

      if(m_isOperationCancelled)
        break;

      for(auto &&pair : *map_sp)
        {
          // first is the rt|dt key
          // second is the pappso::MapTrace

          emit setStatusTextSignal(
            QString("Consolidating color map data from thread %1").arg(iter));

          emit setProgressBarCurrentValueSignal(iter);

          double first_key = pair.first;

          pappso::MapTrace current_maptrace = pair.second;

          // Now search in the member if a rt|dt key was found already.

          Map::iterator double_maptrace_map_iterator;

          double_maptrace_map_iterator =
            m_doubleMapTraceMapSPtr->find(first_key);

          // Tells if the key was already found in the map.
          if(double_maptrace_map_iterator != m_doubleMapTraceMapSPtr->end())
            {
              // qDebug() << "Adding new key value:" << first_key;


              // The map already contained a pair that had the key value ==
              // first_key. All we have to do is combine the new mass
              // spectrum pointed to by the map value into the MapTrace that is
              // pointed to by the map key.

              // Combine the new mass spectrum right into the found MapTrace.

              // qDebug() << "Before combination, map trace map size: "
              //<< double_map_trace_map_iterator->second.size();

              combiner.combine(double_maptrace_map_iterator->second,
                               current_maptrace);
            }
          else
            {

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

              // qDebug() << "No node for a mass spectrum having that same
              // first_key " "was found already:"
              //<< first_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;

              combiner.combine(map_trace, current_maptrace);

              // Finally, store in the map, the map_trace along with its
              // first_key.

              (*m_doubleMapTraceMapSPtr)[first_key] = map_trace;

              // qDebug() << "After combination, map trace map size: " <<
              // map_trace.size();
            }
        }
    }

  // We now need to go through the double map trace map to inspect its
  // characteristics.

  if(!m_doubleMapTraceMapSPtr->size())
    {
      qDebug() << "There are no data in the double maptrace map.";

      return;
    }

  m_colorMapMinKey = m_doubleMapTraceMapSPtr->begin()->first;
  m_colorMapMaxKey = m_doubleMapTraceMapSPtr->rbegin()->first;

  m_colorMapKeyCellCount = m_doubleMapTraceMapSPtr->size();

  for(auto &&pair : *m_doubleMapTraceMapSPtr)
    {
      pappso::MapTrace map_trace = pair.second;

      if(!map_trace.size())
        continue;

      if(map_trace.size() > m_colorMapMzCellCount)
        m_colorMapMzCellCount = map_trace.size();

      if(map_trace.begin()->first < m_colorMapMinMz)
        m_colorMapMinMz = map_trace.begin()->first;

      if(map_trace.rbegin()->first > m_colorMapMaxMz)
        m_colorMapMaxMz = map_trace.rbegin()->first;
    }

  std::chrono::system_clock::time_point chrono_end_time =
    std::chrono::system_clock::now();

  QString chrono_string;

  if(data_kind == pappso::DataKind::rt)
    chrono_string = pappso::Utils::chronoIntervalDebugString(
      "Integration to RT / MZ colormap took:",
      chrono_start_time,
      chrono_end_time);
  else if(data_kind == pappso::DataKind::dt)
    chrono_string = pappso::Utils::chronoIntervalDebugString(
      "Integration to DT / MZ colormap took:",
      chrono_start_time,
      chrono_end_time);
  else
    qFatal("Programming error.");

  emit logTextToConsoleSignal(chrono_string);

  // qDebug().noquote() << chrono_string;

  return;
}


} // namespace minexpert

} // namespace msxps
