/* 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 "MsRunDataSetTreeMassDataIntegratorToDtRtMz.hpp"
#include "ProcessingSpec.hpp"
#include "ProcessingStep.hpp"
#include "ProcessingType.hpp"
#include "BaseMsRunDataSetTreeNodeVisitor.hpp"
#include "TicChromTreeNodeCombinerVisitor.hpp"
#include "IntensityTreeNodeCombinerVisitor.hpp"
#include "RtDtMzColorMapsTreeNodeCombinerVisitor.hpp"
#include "TraceTreeNodeCombinerVisitor.hpp"
#include "MsRunStatisticsTreeNodeVisitor.hpp"
#include "MassSpectrumTreeNodeCombinerVisitor.hpp"
#include "DriftSpectrumTreeNodeCombinerVisitor.hpp"
#include "MultiTreeNodeCombinerVisitor.hpp"


int msRunDataSetTreeMassDataIntegratorToDtRtMzMetaTypeId = qRegisterMetaType<
  msxps::minexpert::MsRunDataSetTreeMassDataIntegratorToDtRtMz>(
  "msxps::minexpert::MsRunDataSetTreeMassDataIntegratorToDtRtMz");

int msRunDataSetTreeMassDataIntegratorToDtRtMzSPtrMetaTypeId =
  qRegisterMetaType<
    msxps::minexpert::MsRunDataSetTreeMassDataIntegratorToDtRtMzSPtr>(
    "msxps::minexpert::MsRunDataSetTreeMassDataIntegratorToDtRtMzSPtr");


namespace msxps
{
namespace minexpert
{


MsRunDataSetTreeMassDataIntegratorToDtRtMz::
  MsRunDataSetTreeMassDataIntegratorToDtRtMz()
{
  qFatal("Cannot be that the default constructor be used.");
  // qDebug() << "Allocating new integrator:" << this;
}


MsRunDataSetTreeMassDataIntegratorToDtRtMz::
  MsRunDataSetTreeMassDataIntegratorToDtRtMz(
    MsRunDataSetCstSPtr ms_run_data_set_csp)
  : MassDataIntegrator(ms_run_data_set_csp)
{
  // Essential that the m_processingFlow member is configured to have the right
  // pointer to the ms run data set.

  m_processingFlow.setMsRunDataSetCstSPtr(ms_run_data_set_csp);
}


MsRunDataSetTreeMassDataIntegratorToDtRtMz::
  MsRunDataSetTreeMassDataIntegratorToDtRtMz(
    MsRunDataSetCstSPtr ms_run_data_set_csp,
    const ProcessingFlow &processing_flow)
  : MassDataIntegrator(ms_run_data_set_csp, processing_flow)
{
  // Essential that the m_processingFlow member is configured to have the right
  // pointer to the ms run data set.
  if(ms_run_data_set_csp != m_processingFlow.getMsRunDataSetCstSPtr())
    qFatal("The pointers should be identical.");
}


MsRunDataSetTreeMassDataIntegratorToDtRtMz::
  MsRunDataSetTreeMassDataIntegratorToDtRtMz(
    const MsRunDataSetTreeMassDataIntegratorToDtRtMz &other)
  : MassDataIntegrator(other)
{
  // Essential that the m_processingFlow member is configured to have the right
  // pointer to the ms run data set.
  if(other.mcsp_msRunDataSet != m_processingFlow.getMsRunDataSetCstSPtr())
    qFatal("The pointers should be identical.");
}


MsRunDataSetTreeMassDataIntegratorToDtRtMz::
  ~MsRunDataSetTreeMassDataIntegratorToDtRtMz()
{
  // qDebug() << "Destroying integrator:" << this;
}

const ProcessingFlow &
MsRunDataSetTreeMassDataIntegratorToDtRtMz::getProcessingFlow() const
{
  return m_processingFlow;
}


void
MsRunDataSetTreeMassDataIntegratorToDtRtMz::appendProcessingStep(
  ProcessingStep *processing_step_p)
{
  if(processing_step_p == nullptr)
    qFatal("The pointer cannot be nullptr.");

  m_processingFlow.push_back(processing_step_p);
}


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


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


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


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


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


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


// Performs integrations to colormap data, like the dt/mz or rt/mz color maps.
bool
MsRunDataSetTreeMassDataIntegratorToDtRtMz::integrateToDtRtMz(
  pappso::DataKind data_kind)
{
  // qDebug();

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

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

  // We need a processing flow to work, and, in particular, a processing step
  // from which to find the specifics of the calculation. The processing flow
  // must have been set by the caller either at construction time or later
  // before using the integrator, that is, calling this function.

  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 *processing_step_p = m_processingFlow.mostRecentStep();

  // Now get a list of the integration types that are stored in the step's
  // map.

  std::vector<ProcessingType> processing_types =
    processing_step_p->processingTypes();

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

  if(data_kind == pappso::DataKind::rt)
    {
      if(!processing_step_p->matches("ANY_TO_RT"))
        qFatal(
          "There should be one ProcessingType = ANY_TO_RT. Program aborted.");
    }
  else if(data_kind == pappso::DataKind::dt)
    {
      if(!processing_step_p->matches("ANY_TO_DT"))
        qFatal(
          "There should be one ProcessingType = ANY_TO_DT. Program aborted.");
    }
  else
    qFatal("Should never encounter this point.");

  // Try to limit the range of MS run data set tree nodes to be iterated through
  // by looking from what node to what other node we need to go to ensure that
  // our integration encompasses the right RT range.

  std::vector<pappso::MsRunDataSetTreeNode *> root_nodes =
    mcsp_msRunDataSet->getMsRunDataSetTreeCstSPtr()->getRootNodes();

  double start_rt = std::numeric_limits<double>::infinity();
  double end_rt   = std::numeric_limits<double>::infinity();

  bool integration_rt = m_processingFlow.innermostRtRange(start_rt, end_rt);

  using Iterator = std::vector<pappso::MsRunDataSetTreeNode *>::const_iterator;
  using Pair     = std::pair<Iterator, Iterator>;

  Pair pair;

  Iterator begin_iterator = root_nodes.begin();
  Iterator end_iterator   = root_nodes.end();

  std::size_t node_count = 0;

  if(integration_rt)
    {
      pair =
        mcsp_msRunDataSet->treeNodeIteratorRangeForRtRange(start_rt, end_rt);

      begin_iterator = pair.first;
      end_iterator   = pair.second;

      node_count = std::distance(begin_iterator, end_iterator);

      qDebug() << "node_count:" << node_count;
    }
  else
    qDebug() << "Not integration_rt";

  if(begin_iterator == root_nodes.end())
    {
      qDebug() << "There is nothing to integrate.";
      return false;
    }

  // Now that we know the non-0 count of nodes to be processed:
  emit setProgressBarMaxValueSignal(node_count);

  // At this point, allocate a visitor that is specific for the calculation of
  // the drift spectrum.

  // But we want to parallelize the computation.

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

  // qDebug() << "nodes_per_thread:" << best_parallel_params.second;

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

  std::vector<RtDtMzColorMapsTreeNodeCombinerVisitorSPtr> visitors;
  std::vector<std::pair<Iterator, Iterator>> iterators =
    calculateMsRunDataSetTreeNodeIteratorPairs(begin_iterator,
                                               end_iterator,
                                               best_parallel_params.first,
                                               best_parallel_params.second);

  // For each available thread, allocate a new visitor. We also configure
  // which root nodes it should handle as two (begin,end) iterators to the ms
  // run data set tree. We push back the pair of iterators to the vector of
  // iterators.

  for(std::size_t iter = 0; iter < iterators.size(); ++iter)
    {
      // qDebug() << "thread index:" << iter;

      RtDtMzColorMapsTreeNodeCombinerVisitorSPtr visitor_sp =
        std::make_shared<RtDtMzColorMapsTreeNodeCombinerVisitor>(
          mcsp_msRunDataSet, m_processingFlow, data_kind);

      // We want to be able to intercept any cancellation of the operation.

      connect(
        this,
        &MsRunDataSetTreeMassDataIntegratorToDtRtMz::cancelOperationSignal,
        [visitor_sp]() { visitor_sp->cancelOperation(); });

      // The visitor gets the number of nodes to process from the data set
      // tree node. Capture that number and relay it.

      connect(
        visitor_sp.get(),
        &RtDtMzColorMapsTreeNodeCombinerVisitor::setProgressBarMaxValueSignal,
        [this](std::size_t number_of_nodes_to_process) {
          // qDebug() << "Should emit numberOfNodesToProcessSignal";
          emit setProgressBarMaxValueSignal(number_of_nodes_to_process);
        });

      // The visitor emits the signal to tell that the currently iterated node
      // has number_of_nodes_to_process children to process. Because the visitor
      // might operate in a thread and that there might be other threads
      // handling other nodes, we do not consider that the
      // number_of_nodes_to_process value is the total number of nodes to
      // process but only the number of nodes that the visitor is handling. We
      // thus update the max progress bar value by number_of_nodes_to_process.
      // In a typical setting, the user of the signal is the monitoring object,
      // be it a non-gui object or the TaskMonitorCompositeWidget.
      connect(visitor_sp.get(),
              &RtDtMzColorMapsTreeNodeCombinerVisitor::
                incrementProgressBarMaxValueSignal,
              [this](std::size_t number_of_nodes_to_process) {
                emit incrementProgressBarMaxValueSignal(
                  number_of_nodes_to_process);
              });

      connect(visitor_sp.get(),
              &RtDtMzColorMapsTreeNodeCombinerVisitor::
                setProgressBarCurrentValueSignal,
              [this](std::size_t number_of_processed_nodes) {
                // qDebug() << "Should emit setProgressBarCurrentValue";
                emit setProgressBarCurrentValueSignal(
                  number_of_processed_nodes);
              });

      connect(visitor_sp.get(),
              &RtDtMzColorMapsTreeNodeCombinerVisitor::
                incrementProgressBarCurrentValueAndSetStatusTextSignal,
              [this](std::size_t increment, QString text) {
                emit incrementProgressBarCurrentValueAndSetStatusTextSignal(
                  increment, text);
              });

      connect(visitor_sp.get(),
              &RtDtMzColorMapsTreeNodeCombinerVisitor::setStatusTextSignal,
              [this](QString text) {
                // qDebug() << "Should emit text:" << text;
                emit setStatusTextSignal(text);
              });

      connect(visitor_sp.get(),
              &RtDtMzColorMapsTreeNodeCombinerVisitor::
                setStatusTextAndCurrentValueSignal,
              [this](QString text, std::size_t value) {
                // qDebug() << "Should emit text:" << text;
                emit setStatusTextAndCurrentValueSignal(text, value);
              });

      visitors.push_back(visitor_sp);
    }

  // Now perform a parallel iteration in the various visitors and ask them to
  // work on the matching ms run data set iterators pair.
  omp_set_num_threads(visitors.size());
#pragma omp parallel for
  for(std::size_t iter = 0; iter < visitors.size(); ++iter)
    {
      auto visitor_sp = visitors.at(iter);

      // qDebug() << "Visitor:" << visitor_sp.get() << "at index:" << iter
      //<< "Executing from thread:" << QThread::currentThreadId();

      mcsp_msRunDataSet->msp_msRunDataSetTree->accept(
        *(visitor_sp.get()),
        iterators.at(iter).first,
        iterators.at(iter).second);
    }
  // End of
  // #pragma omp parallel for

  // At this point we need to combine all the visitor-contained map traces
  // into a single trace. We make the combination into the member MapTrace
  // object.

  // qDebug() << "End of the iteration in the visitors";

  // If the task was cancelled, the monitor widget was locked. We need to
  // unlock it.
  emit unlockTaskMonitorCompositeWidgetSignal();
  emit setupProgressBarSignal(0, visitors.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;

  pappso::TracePlusCombiner trace_plus_combiner;

  for(std::size_t iter = 0; iter < visitors.size(); ++iter)
    {
      if(m_isOperationCancelled)
        break;

      auto &&visitor_sp = visitors.at(iter);

      // Each visitor has a std::map<double, MapTrace> map to relate the rt|dt
      // value to a MapTrace that was the receptacle to the combinations of all
      // the mass spectra by a given rt|dt value. We need to ensure that all the
      // MapTrace entities by the same rt|dt value get combined together.

      // We use the member m_doubleMapTraceMapSPtr to craft the result map.

      const std::map<double, pappso::MapTrace> visitor_double_map_trace_map =
        visitor_sp->getDoubleMapTraceMap();

      std::pair<double, pappso::MapTrace> visitor_double_map_trace_map_pair;

      // qDebug() << "In the consolidating loop, iter:" << iter
      //<< "with a map trace of this size : "
      //<< visitor_double_map_trace_map.size();

      if(visitor_sp->getDataKind() == pappso::DataKind::rt)
        {
          emit setStatusTextSignal(
            QString(
              "Consolidating retention time-mapped spectra from thread %1")
              .arg(iter + 1));
        }
      else if(visitor_sp->getDataKind() == pappso::DataKind::dt)
        {
          emit setStatusTextSignal(
            QString("Consolidating drift time-mapped spectra from thread %1")
              .arg(iter + 1));
        }

      emit setProgressBarCurrentValueSignal(iter + 1);

      for(auto &&visitor_double_map_trace_map_pair :
          visitor_double_map_trace_map)
        {
          double rt_or_dt_key = visitor_double_map_trace_map_pair.first;

          // Did we already encounter a map trace map by rt_or_dt ?

          std::map<double, pappso::MapTrace>::iterator
            integrator_double_map_trace_map_iterator;

          integrator_double_map_trace_map_iterator =
            m_doubleMapTraceMapSPtr->find(rt_or_dt_key);

          pappso::TracePlusCombiner combiner(0 /* decimal places */);

          // Tells if the key was already found in the map.
          if(integrator_double_map_trace_map_iterator !=
             m_doubleMapTraceMapSPtr->end())
            {
              // *this integrator map already contained a pair that had the key
              // value == rt_or_dt_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.

              combiner.combine(
                integrator_double_map_trace_map_iterator->second,
                visitor_double_map_trace_map_pair.second.toTrace());
            }
          else
            {

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

              // Instantiate a new MapTrace in which we'll combine this and all
              // the future mass spectra having been acquired at this same rt|dt
              // value.

              pappso::MapTrace map_trace;

              combiner.combine(
                map_trace, visitor_double_map_trace_map_pair.second.toTrace());

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

              (*m_doubleMapTraceMapSPtr)[rt_or_dt_key] = map_trace;
            }
        }
      // End of
      // for(auto &&visitor_double_map_trace_map_pair : double_map_trace_map)

      // At this point we have finished processing all the items of the double
      // map trace map sitting in the iterated visitor. Go on with next visitor.
    }
  // End of
  // for(std::size_t iter = 0; iter < visitors.size(); ++iter)

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

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

  // At this point we have finished going through all the visitors. Our work is
  // finished. The m_doubleMapTraceMap now has items for each rt|dt value
  // present in the processed data. For each such value, the corresponding
  // MapTrace contains the combinatorial mass spectrum of all the mass spectra
  // obtained for each rt|dt value.

  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 TIC/XIC chrom / mass spectrum took:",
      chrono_start_time,
      chrono_end_time);
  else if(data_kind == pappso::DataKind::dt)
    chrono_string = pappso::Utils::chronoIntervalDebugString(
      "Integration to drift spectrum / mass spectrum took:",
      chrono_start_time,
      chrono_end_time);

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

  return true;
}


} // namespace minexpert

} // namespace msxps
