/* 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/IsotopicDataUserConfigHandler.hpp"


/////////////////////// libXpertMassGui includes
#include <MsXpS/libXpertMassCore/Processing/DeconvolutedMassIdMatcher.hpp>

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

namespace MsXpS
{
namespace libXpertMassCore
{
namespace Processing
{


/*!
\struct MsXpS::libXpertMassCore::Processing::DeconvolutedMassIdMatcherParameters
\inmodule libXpertMassCore
\ingroup XpertMassCoreProcessing
\inheaderfile MsXpS/libXpertMassCore/Processing/DeconvolutedMassIdMatcher.hpp

\brief The DeconvolutedMassIdMatcherParameters structure holds all the
parameters required to configure a DeconvolutedMassIdMatcher instance.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::DeconvolutedMassIdMatcherParameters::dictionaryFilePath

\brief Path to the file that contains the dictionary data that allows the
identification of analytes using deconvoluted features.

The dictionary data has the following tab-separated-value (TSV) format with at
least the three compulsory columns:

\list

\li Id: the identity of the analyte (used to label the analyte in the mass
spectrum)
\li Structure: the detailed structure of the analyte (for reporting to the user
only)
\li Formula: the formula of the analyte in its neutral deionized form. This
formula is used in real time to compute the mass of the analyte.

\endlist
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::DeconvolutedMassIdMatcherParameters::isotopicDataFilePath

\brief The path to the file that contains the isotopic data that are used to
compute the mass of the analyte starting from its formula.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::DeconvolutedMassIdMatcherParameters::processingDetailsLogFilePath

\brief Path the the file where all the computation details are logged to.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::DeconvolutedMassIdMatcherParameters::massType

\brief The mass type (Enums::MassType) of the analyte's mass that needs to be
computed. By default the mass type is Enums::MassType::MONO.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::DeconvolutedMassIdMatcherParameters::massTolerancePpm

\brief The tolerance in parts-per-million (ppm) that is accepted for any mass
matching.
*/

/*!
\brief Constructs an unitialized instance.
*/
DeconvolutedMassIdMatcherParameters::DeconvolutedMassIdMatcherParameters()
{
}

/*!
\brief Constructs an instance fully initialized using the parameters.

\list

\li \a dictionary_file_path: inializes dictionaryFilePath

\li \a isotopic_data_file_path: inializes isotopicDataFilePath

\li \a processing_details_log_file_path: inializes processingDetailsLogFilePath

\li \a mass_type: inializes massType

\li \a mass_tolerance_ppm: inializes massTolerancePpm

\endlist
*/
DeconvolutedMassIdMatcherParameters::DeconvolutedMassIdMatcherParameters(
  const QString &dictionary_file_path,
  const QString &isotopic_data_file_path,
  const QString &processing_details_log_file_path,
  Enums::MassType mass_type,
  double mass_tolerance_ppm)
  : dictionaryFilePath(dictionary_file_path),
    isotopicDataFilePath(isotopic_data_file_path),
    processingDetailsLogFilePath(processing_details_log_file_path),
    massType(mass_type),
    massTolerancePpm(mass_tolerance_ppm)
{
}

/*!
\brief Constructs an instance initializing it using \a other as a template.
*/
DeconvolutedMassIdMatcherParameters::DeconvolutedMassIdMatcherParameters(
  const DeconvolutedMassIdMatcherParameters &other)
  : dictionaryFilePath(other.dictionaryFilePath),
    isotopicDataFilePath(other.isotopicDataFilePath),
    processingDetailsLogFilePath(other.processingDetailsLogFilePath),
    massType(other.massType),
    massTolerancePpm(other.massTolerancePpm)
{
}

/*!
\brief Initializes this instance using \a other as a template and returns a
reference to this instance.
*/
DeconvolutedMassIdMatcherParameters &
DeconvolutedMassIdMatcherParameters::operator=(
  const DeconvolutedMassIdMatcherParameters &other)
{
  if(&other == this)
    return *this;

  dictionaryFilePath           = other.dictionaryFilePath;
  isotopicDataFilePath         = other.isotopicDataFilePath;
  processingDetailsLogFilePath = other.processingDetailsLogFilePath;
  massType                     = other.massType;
  massTolerancePpm             = other.massTolerancePpm;

  return *this;
}

/*!
\brief Crafts a string documenting this instance and returns it.
*/
QString
DeconvolutedMassIdMatcherParameters::toString() const
{
  QString text = "DeconvolutedMassIdMatcher parameters:\n";

  text += QString("Dictionary file path: %1\n").arg(dictionaryFilePath);
  text += QString("Isotopic data file path: %1\n").arg(isotopicDataFilePath);
  text += QString("Mass type: %1\n")
            .arg(massType == Enums::MassType::MONO ? "MONO" : "AVG");
  text +=
    QString("Mass tolerance (ppm): %1\n").arg(massTolerancePpm, 0, 'f', 0);

  return text;
}

/*!
\struct MsXpS::libXpertMassCore::Processing::DeconvolutedMassIdMatcher::Data
\inmodule libXpertMassCore
\ingroup XpertMassCoreProcessing
\inheaderfile MsXpS/libXpertMassCore/Processing/DeconvolutedMassIdMatcher.hpp

\brief The Data struct provides all the required data elements for the matcher
to perform its tasks. Also, these elements are made available back to the
matcher user so as to continue the processing even after the thread has
returned.
*/

// clang-format off
/*!
\struct MsXpS::libXpertMassCore::Processing::DeconvolutedMassIdMatcher::MassIdMatch
\inmodule libXpertMassCore
\ingroup XpertMassCoreProcessing
\inheaderfile MsXpS/libXpertMassCore/Processing/DeconvolutedMassIdMatcher.hpp

\brief The MassIdMatch struct stores all the details about a successful Id
match.
*/
// clang-format on

/*!
\class MsXpS::libXpertMassCore::Processing::DeconvolutedMassIdMatcher
\inmodule libXpertMassCore
\ingroup XpertMassCoreProcessing
\inheaderfile MsXpS/libXpertMassCore/Processing/DeconvolutedMassIdMatcher.hpp

\brief The DeconvolutedMassIdMatcher class provides the mechanics that performs
matches between analyte IDs and chemical formulas provided in a dictionary file.

The dictionary file is a speadsheet file (CSV with TAB delimiter) that must
have, amongs any number of other colums, three columns with the following
headers:

"Id"     "Structure"      "Formula"

Each row of the spreadsheet file is thus formmatted like this:

"Gly"    "Gly"            "C2H5N1O2"

As shown, all the textual content must be enclosed in '"' (double quote)
characters.

The "Id" column contents are going to be used to label the mass peaks in the
mass spectrum whenever a deconvoluted feature could be matched to an analyte's
mass computed starting from the contents of the "Formula" column.

From the spreadsheet program, make sure to save as CSV file with the following
settings:

\list

\li {TAB} as the delimiter
\li '"' (double quote) as the text quoting symbol

\endlist

\note If the file saving to CSV is not performed with these settings, the export
might fail silently with missing text parts, especially if the text contains '['
or '{' characters.

The three fundamental compulsory columns in a spreadsheet (CSV-TAB-formatted)
file that need to be present are "Id", "Structure", and "Formula".
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::DeconvolutedMassIdMatcher::m_params

\brief The parameters used to configure this DeconvolutedMassIdMatcher instance.
*/


/*!
\brief Constructs an instance using \a parameters, \a
deconvoluted_features_qsp and \a parent.

If \l{m_params}.processingDetailsLogFilePath contains a valid file path, the
logging feature is activated and all processing details will be logged to the
corresponding file.
*/
DeconvolutedMassIdMatcher::DeconvolutedMassIdMatcher(
  DeconvolutedMassIdMatcherParameters &parameters,
  QSharedPointer<QList<DeconvolutedFeature>> deconvoluted_features_qsp,
  QObject *parent)
  : MonitoredTaskInterface(parent),
    m_params(parameters),
    mqsp_deconvolutedFeatures(deconvoluted_features_qsp)
{
  if(!m_params.processingDetailsLogFilePath.isEmpty())
    {
      mp_logFile = new QFile(m_params.processingDetailsLogFilePath);
      if(mp_logFile == nullptr)
        qFatal() << "Failed to create a new log file instance.";

      if(mp_logFile->open(QIODevice::WriteOnly | QIODevice::Truncate |
                          QIODevice::Text))
        {
          mp_logTextStream = new QTextStream(mp_logFile);
          *mp_logTextStream << "This is a processing details log file.\n\n";
          mp_logTextStream->flush();
        }
      else
        qFatal() << "Failed to open the log file for writing.";
    }
}

/*!
\brief Destructs this DeconvolutedMassIdMatcher instance.
*/
DeconvolutedMassIdMatcher::~DeconvolutedMassIdMatcher()
{
  // 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;
}

/*!
\brief Set the \a data to this instance.
*/
void
DeconvolutedMassIdMatcher::setData(const Data &data)
{
  m_data = data;
}

/*!
\brief Returns the container of MassIdMatch instances.
*/
const QList<QSharedPointer<DeconvolutedMassIdMatcher::MassIdMatch>> &
DeconvolutedMassIdMatcher::getMassIdMatches() const
{
  return m_massIdMatches;
}

/*!
\brief Loads and parses the dictionary file and logs any error to \a
error_list_p if non-nullptr.

Returns the number dictionary rows successfully parsed, -1 if a fatal error
occurred.

If any row fails to be parsed and if \a error_list_p is not nullptr, that row is
documented in the ErrorList.
*/
int
DeconvolutedMassIdMatcher::loadDictionaryFile(ErrorList *error_list_p)
{
  // First determine the number of lines in the file
  qint64 line_count = Utils::countLinesInTextFile(m_params.dictionaryFilePath);

  qDebug() << "There are" << line_count << "lines in the dictionary file";
  emit taskSetMinMaxValuesSignal(0, line_count);

  QString message;
  QString processing_details_string =
    QString("Loading dictionary (file path: %1)\n")
      .arg(m_params.dictionaryFilePath);

  if(m_params.dictionaryFilePath.isEmpty())
    {
      message = "The dictionary file path is empty.";

      qDebug() << message;
      if(error_list_p)
        error_list_p->append(message);

      processing_details_string += message + "\n";
      logProcessingDetails(processing_details_string);

      emit taskSetStatusMessageSignal(message);

      return -1;
    }

  QFile file(m_params.dictionaryFilePath);

  if(!file.exists())
    {
      message = "The dictionary file does not exist.";

      qDebug() << message;
      if(error_list_p)
        error_list_p->append(message);

      processing_details_string += message + "\n";
      logProcessingDetails(processing_details_string);

      emit taskSetStatusMessageSignal(message);

      return -1;
    }

  if(!file.open(QFile::ReadOnly))
    {
      message = "Failed to open the dictionary file for reading.";

      qDebug() << message;
      if(error_list_p)
        error_list_p->append(message);

      processing_details_string += message + "\n";
      logProcessingDetails(processing_details_string);

      emit taskSetStatusMessageSignal(message);

      return -1;
    }

  QTextStream in_stream(&file);
  QString line;

  in_stream.readLineInto(&line);

  if(line.isEmpty())
    {
      message =
        "The very first line of the file is empty. It should have contained "
        "the spreadsheet table header.";

      qDebug() << message;
      if(error_list_p)
        error_list_p->append(message);

      processing_details_string += message + "\n";
      logProcessingDetails(processing_details_string);

      emit taskSetStatusMessageSignal(message);

      file.close();
      return -1;
    }

  line.remove("\"");

  emit taskSetStatusMessageAndCurrentValueSignal(
    QString("Iterating into dictionary file line %1").arg(1), 1);

  qDebug().noquote() << "The very first line of the file:" << line;

  QStringList line_string_list = line.split("\t");

  if(!line_string_list.contains("Id") ||
     !line_string_list.contains("Structure") ||
     !line_string_list.contains("Formula"))
    {
      message =
        QString(
          "The very first line:\n%1\nof the file does not contain a proper "
          "spreadsheet table header. It should have contained at least \"Id\", "
          "\"Structure\" and \"Formula\"")
          .arg(line);

      qDebug().noquote() << message;
      if(error_list_p)
        error_list_p->append(message);

      processing_details_string += message + "\n";
      logProcessingDetails(processing_details_string);

      message = "The dictionary file is faulty.";
      emit taskSetStatusMessageSignal(message);

      file.close();
      return -1;
    }

  // At this point, check the column structure of the spreadsheet:

  int id_index        = line_string_list.indexOf("Id");
  int structure_index = line_string_list.indexOf("Structure");
  int formula_index   = line_string_list.indexOf("Formula");

  message =
    QString(
      "The header index for id: %1 - for structure: %2 - for formula: %3\n")
      .arg(id_index)
      .arg(structure_index)
      .arg(formula_index);

  processing_details_string += message;
  qDebug() << message;

  processing_details_string +=
    "Now reading into all the remaining lines of the dictionary file.";

  // These variables allow for updating the task monitor progress bar
  // less frequently (one over 3 values if the divisor is set to 3).
  int iteration_counter_divisor = 50;

  qint64 iterated_line_number = 2;

  while(in_stream.readLineInto(&line))
    {
      // Do not output task status string at each iteration, but
      // every other iteration_counter_divisor iteration.

      int iteration_counter_fraction_remainder =
        iterated_line_number % iteration_counter_divisor;

      // Only update status every other iteration_counter_divisor iteration.
      if(!iteration_counter_fraction_remainder)
        emit taskSetStatusMessageAndCurrentValueSignal(
          QString("Iterating into dictionary file line %1")
            .arg(iterated_line_number),
          iterated_line_number);

      if(line.startsWith("\n") || line.startsWith("#"))
        continue;

      line.remove("\"");

      line_string_list = line.split("\t");

      QString id        = line_string_list.at(id_index);
      QString structure = line_string_list.at(structure_index);
      QString formula   = line_string_list.at(formula_index);

      if(id.isEmpty() || structure.isEmpty() || formula.isEmpty())
        {
          message = QString("Line \"%1\" is not acceptable.").arg(line);
          qDebug() << message;
          processing_details_string += message + "\n";

          if(error_list_p)
            error_list_p->append(message);

          continue;
        }

      Formula the_formula(formula);
      bool ok          = false;
      double mono_mass = 0;
      double avg_mass  = 0;
      double times     = 1;

      the_formula.accountMasses(
        ok, mcsp_isotopicData, mono_mass, avg_mass, times);

      if(!ok)
        {
          message =
            QString("Failed to account masses for formula %1\n").arg(formula);
          qDebug() << message;
          if(error_list_p)
            error_list_p->append(message);

          processing_details_string += message + "\n";

          continue;
        }

      double mass =
        (m_params.massType == Enums::MassType::MONO ? mono_mass : avg_mass);

      QSharedPointer<MassIdMatch> mass_id_match_qsp =
        QSharedPointer<MassIdMatch>::create(
          id, structure, formula, nullptr, mass);

      m_massIdMatches.append(mass_id_match_qsp);

      ++iterated_line_number;
    }

  message = QString("Finished reading %1 - items stored: %2")
              .arg(m_params.dictionaryFilePath)
              .arg(m_massIdMatches.size());
  qDebug() << message;
  processing_details_string += message + "\n";

  // If we did not read a single dictionary item, then this is an error.

  if(!m_massIdMatches.size())
    {
      message = QString("Could not read a single dictionary item.");
      qDebug() << message;
      if(error_list_p)
        error_list_p->append(message);

      processing_details_string += message + "\n";
    }
  else
    {
      message = QString("The very first dictionary item:\n%1\n")
                  .arg(m_massIdMatches.first()->toString());
      message += QString("The very last dictionary item:\n%1")
                   .arg(m_massIdMatches.last()->toString());

      processing_details_string += message + "\n";

      qDebug() << message;
    }

  logProcessingDetails(processing_details_string);

  return m_massIdMatches.size();
}

/*!
\brief Removes any MassIdMatch item that has not been used for an actual
successful match .
*/
void
DeconvolutedMassIdMatcher::removeUnusedMassIdMatches()
{
  // We now should remove all the MassIdMatch items that have not been used
  // for any successful match.
  // qDebug() << "There are" << m_massIdMatches.size()
  //          << "dictionary items before.";

  auto it = std::remove_if(m_massIdMatches.begin(),
                           m_massIdMatches.end(),
                           [](const QSharedPointer<MassIdMatch> &ptr) {
                             return ptr->massError == 0;
                           });

  m_massIdMatches.erase(it, m_massIdMatches.end());

  // qDebug() << "There are" << m_massIdMatches.size()
  //          << "dictionary items after.";
}

/*!
\brief Performs the matches and returns the successful match count.
*/
int
DeconvolutedMassIdMatcher::performMatches()
{
  QString message = "Performing matches";

  QString processing_details_string = message + "n\n";

  emit taskSetStatusMessageSignal(message);

  QElapsedTimer timer;
  timer.start();

  // Setup the monitored task interface.

  emit taskSetMinMaxValuesSignal(0, mqsp_deconvolutedFeatures->size());

  // These variables allow for updating the task monitor progress bar
  // less frequently (one over 3 values if the divisor is set to 3).
  int iteration_counter_divisor            = 2;
  int iteration_counter_fraction_remainder = 0;

  for(int iter = 0; iter < mqsp_deconvolutedFeatures->size(); ++iter)
    // foreach(const DeconvolutedFeature &deconvoluted_feature,
    //         *mqsp_deconvolutedFeatures)
    {
      if(m_cancelledTask)
        {
          emit taskSetStatusMessageSignal("The task was cancelled by the user.");
          removeUnusedMassIdMatches();
          return m_massIdMatches.size();
        }

      // Do not output task status string at each iteration, but
      // every other iteration_counter_divisor iteration.

      iteration_counter_fraction_remainder = iter % iteration_counter_divisor;
      // Only update status every other iteration_counter_divisor iteration.
      if(!iteration_counter_fraction_remainder)
        emit taskSetStatusMessageAndCurrentValueSignal(
          QString("Iterating into deconvoluted feature %1").arg(iter), iter);

      // emit taskStatusMessageAndCurrentValueSignal(
      //   QString("Iterating into deconvoluted feature %1").arg(iter), iter);

      const DeconvolutedFeature &deconvoluted_feature =
        mqsp_deconvolutedFeatures->at(iter);

      double mass_to_test = deconvoluted_feature.neutralMass;

      // We are iterating into a deconvoluted mass and we now look into
      // the whole set of dictionary items that relate a given formula to
      // a given identity. Account for the mass tolerance.

      // No worries for the charges, as these are not accounted for here,
      // since the deconvoluted feature's mass is Mr (that is, neutral) and
      // the dictionary items have formulas for neutral analytes.

      // #if 0

      // Save original, set temporary, restore later
      auto *pool = QThreadPool::globalInstance();
      int oldMax = pool->maxThreadCount();

      // FIXME - for my 20 thread computer, this is enough
      // for a dictionary file of 60k lines.
      pool->setMaxThreadCount(5);

      // New parallel version - frankly for the moment there is no benefit
      // with a dictionary file of 60 000 lines.

      // Thread-local result structure
      struct MatchResult
      {
        int localMatchCount = 0;
        QString localDetails;
      };

      DeconvolutedMassIdMatcherParameters local_params(m_params);

      QList<MatchResult> results(m_massIdMatches.size());

      // qDebug() << "=== Thread Pool Info ===";
      // qDebug() << "Max thread count:"
      //          << QThreadPool::globalInstance()->maxThreadCount();
      // qDebug() << "Active thread count:"
      //          << QThreadPool::globalInstance()->activeThreadCount();
      // qDebug() << "Current thread:" << QThread::currentThreadId();
      // qDebug() << "Ideal thread count (QThread::idealThreadCount()):"
      //          << QThread::idealThreadCount();

      // ✅ Use mapped() instead of map() - it returns
      // QFuture<QList<MatchResult>>

      // static QMutex debug_log_mutex;

      auto future = QtConcurrent::mapped(
        std::as_const(m_massIdMatches),
        [&mass_to_test, &local_params, &deconvoluted_feature](
          const QSharedPointer<MassIdMatch>
            mass_id_match_qsp) // ← pass by value or const-ref
        {
          // QMutexLocker locker(&debug_log_mutex);
          // qDebug() << "[FUNCTOR] Running on thread:"
          //          << QThread::currentThreadId();
          // locker.unlock();

          MatchResult result;

          double left_tolerance_side =
            mass_id_match_qsp->mass -
            ((mass_id_match_qsp->mass * local_params.massTolerancePpm) / 1e6 /
             2);
          double right_tolerance_side =
            mass_id_match_qsp->mass +
            ((mass_id_match_qsp->mass * local_params.massTolerancePpm) / 1e6 /
             2);

          if(mass_to_test >= left_tolerance_side &&
             mass_to_test <= right_tolerance_side)
            {
              // Safe: each thread modifies its own MassIdMatch pointer
              mass_id_match_qsp->deconvolutedFeatureQsp =
                QSharedPointer<DeconvolutedFeature>::create(
                  deconvoluted_feature);
              mass_id_match_qsp->massError =
                mass_to_test - mass_id_match_qsp->mass;

              result.localMatchCount = 1;
              result.localDetails    = mass_id_match_qsp->toString() + "\n";
            }
          return result; // ✅ This return value is NOW collected!
        });

      // Wait for completion and get results
      future.waitForFinished();

      results = future.results(); // ✅ Collect the returned values

      // Reduce: combine thread-local results (single-threaded)
      int match_count = 0;
      for(const auto &result : std::as_const(results))
        {
          match_count               += result.localMatchCount;
          processing_details_string += result.localDetails;
        }

      message =
        QString("Finished performing the matches. Found %1 matches.\n\n")
          .arg(match_count);

      // Output logs after parallel section
      if(!processing_details_string.isEmpty())
        {
          qDebug().noquote() << "\n" << processing_details_string;
        }

      pool->setMaxThreadCount(oldMax); // Restore

      // #endif

#if 0
      // First serial  version - frankly this is sufficient at the moment.
      foreach(QSharedPointer<MassIdMatch> mass_id_match_qsp, m_massIdMatches)
        {
          double left_tolerance_side =
            mass_id_match_qsp->mass -
            ((mass_id_match_qsp->mass * m_params.massTolerancePpm) / 1e6 / 2);
          double right_tolerance_side =
            mass_id_match_qsp->mass +
            ((mass_id_match_qsp->mass * m_params.massTolerancePpm) / 1e6 / 2);

          if(mass_to_test >= left_tolerance_side &&
             mass_to_test <= right_tolerance_side)
            {
              // The match is successful. Document it.
              mass_id_match_qsp->deconvolutedFeatureQsp =
                QSharedPointer<DeconvolutedFeature>::create(
                  deconvoluted_feature);

              mass_id_match_qsp->massError =
                mass_to_test - mass_id_match_qsp->mass;

              message                    = mass_id_match_qsp->toString() + "\n";
              processing_details_string += message + "\n";

              // qDebug().noquote() << "\n" << message;
            }
          // else
          //   qDebug().noquote()
          //     << "Failed Mass vs Id match:" << mass_id_match_qsp->toString();
        }
      // End of
      // foreach(QSharedPointer<MassIdMatch> mass_id_match_qsp, m_massIdMatches)
      // That is, we finished iterating into the dictionary items.
#endif
    }
  // End of
  //   foreach(const DeconvolutedFeature &deconvoluted_feature,
  //           *mqsp_deconvolutedFeatures)
  // That is, we finished iterating in the deconvoluted features.

  qDebug() << "Parallel execution time:" << timer.elapsed() << "ms";

  removeUnusedMassIdMatches();

  int match_count = m_massIdMatches.size();

  message = QString("Finished performing the matches. Found %1 matches.\n\n")
              .arg(match_count);
  qDebug() << message;
  processing_details_string += message + "\n";
  logProcessingDetails(processing_details_string);

  return match_count;
}

/*!
\brief Runs the whole process.

The steps are the following:

\list

\li Loads the isotopic data file
\li Loads the dictionary file
\li Performs the matches

\endlist

\sa performMatches()
*/
void
DeconvolutedMassIdMatcher::run()
{
  if(m_isRunning)
    {
      qWarning() << "Matcher already running. Ignoring duplicate start.";
      return;
    }

  // We do not have values of progress bar limits.
  emit taskSetMinMaxValuesSignal(-1, -1);

  QString message =
    "Start matching deconvoluted masses with analyte identities";
  QString processing_details_string = message + "\n";
  qDebug() << message;

  emit taskSetStatusMessageSignal(message);

  // Since we provide the user with the ability to load a specific
  // isotopic data file, that needs to be IsotopicDataUserConfigHandler
  // because otherwise (IsotopicDataLibraryHandler), the file
  // name would be of no use: the data would be loaded from the IsoSpec
  // library material (natural abundances for all the isotopes).
  libXpertMassCore::IsotopicDataUserConfigHandler isotopic_data_handler;

  qDebug() << "The isotopic data file path:" << m_params.isotopicDataFilePath;

  emit taskSetStatusMessageSignal("Loading the isotopes");

  qsizetype loaded_isotope_count =
    isotopic_data_handler.loadData(m_params.isotopicDataFilePath);

  if(!loaded_isotope_count)
    {
      QString msg = QString(
                      "Failed to load any isotopic data item from\n%1\n"
                      "Please check the isotopic data file path setting.")
                      .arg(m_params.isotopicDataFilePath);
      qDebug() << msg;

      emit taskSetStatusMessageAndCurrentValueSignal(
        "Failed to load the isotopic data from file", 0);
      emit taskFinishedSignal();
    }

  mcsp_isotopicData = isotopic_data_handler.getIsotopicData();

  if(mcsp_isotopicData == nullptr || mcsp_isotopicData.get() == nullptr)
    {
      message = "There are no isotopic data available.";
      qDebug() << message;
      if(m_data.errorList != nullptr)
        m_data.errorList->append(message);

      processing_details_string += message + "\n";
      logProcessingDetails(processing_details_string);

      emit taskSetStatusMessageSignal(message);
      emit taskFinishedSignal();

      return;
    }

  emit taskSetStatusMessageSignal("Done loading the isotopes");

  if(mqsp_deconvolutedFeatures == nullptr ||
     mqsp_deconvolutedFeatures.get() == nullptr)
    {
      message = "There are no deconvoluted features.";

      qDebug() << message;
      if(m_data.errorList != nullptr)
        m_data.errorList->append(message);

      processing_details_string += message + "\n";
      logProcessingDetails(processing_details_string);

      emit taskSetStatusMessageSignal(message);
      emit taskFinishedSignal();

      return;
    }

  emit taskSetStatusMessageSignal(
    "Loading the dictionary file and computing masses");

  ErrorList error_list;

  if(!loadDictionaryFile(&error_list))
    {
      message = "Failed to read the dictionary file";
      emit taskSetStatusMessageSignal(message);

      if(m_data.errorList != nullptr)
        {
          message += QString(
                       ", with the following errors:\n"
                       "%1\n")
                       .arg(Utils::joinErrorList(error_list, "\n"));
        }

      qDebug() << message;
      processing_details_string += message + "\n";
      logProcessingDetails(processing_details_string);

      emit taskFinishedSignal();

      return;
    }

  // Sort the loaded dictionary items by mass in ascending order.

  message = "Sorting the loaded dictionary items in ascending order.";
  qDebug() << message;
  processing_details_string += message + "\n";


  emit taskSetStatusMessageSignal(message);

  // Sort in ascending order by mass
  std::sort(m_massIdMatches.begin(),
            m_massIdMatches.end(),
            [](const QSharedPointer<MassIdMatch> &a,
               const QSharedPointer<MassIdMatch> &b) {
              return a->mass < b->mass;
            });

  emit taskSetStatusMessageSignal(
    "Done loading the dictionary file and computing masses");

  // At this point we have all the dictionary data required for the matches.
  // We can iterate in the deconvoluted features and make the matches.

  int match_count = 0;

  message = "Now performing the matches";
  emit taskSetStatusMessageSignal(message);

  match_count = performMatches();

  if(!match_count)
    {
      message = "Failed to perform a single match.";
      qDebug() << message;
      processing_details_string += message + "\n";
      logProcessingDetails(processing_details_string);

      emit taskSetStatusMessageSignal(message);
    }

  message = QString("Could perform %1 matches").arg(match_count);

  qDebug() << message;

  emit taskSetStatusMessageSignal(message);
  emit taskFinishedSignal();
  emit finishedSignal(m_data.initialSourcePlottable,
                      m_data.sampleName,
                      m_data.labelColorString,
                      m_data.labelFontString,
                      m_data.labelListName,
                      m_data.shouldWriteLabels,
                      m_data.shouldAddLabelToList);
}

/*!
\brief Cancels the task.
*/
void
DeconvolutedMassIdMatcher::cancelTask()

{
  m_cancelledTask = true;
}

} // namespace Processing
} // namespace libXpertMassCore
} // namespace MsXpS
