/* BEGIN software license
 *
 * MsXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright (C) 2009--2026 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
 */

#pragma once

/////////////////////// stdlib includes


/////////////////////// Qt includes
#include <QList>
#include <QMap>
#include <QFileInfo>


/////////////////////// pappsomspp includes
#include "pappsomspp/core/trace/trace.h"


/////////////////////// Local includes
#include "MsXpS/export-import-config.h"
#include "MsXpS/libXpertMassCore/Averagine.hpp"

namespace MsXpS
{
namespace libXpertMassCore
{


// National Institute of Standards and Technology - CODATA 2018
// 2018, Reviews of Modern Physics
static constexpr double PROTON_MASS           = 1.007276466621;
// A neutron not engaged in the formation of an atom.
static constexpr double NEUTRON_MASS          = 1.00866491606;
// A neutron participating into an atom (mass defect for Carbon).
static constexpr double ISOTOPIC_NEUTRON_MASS = 1.0033548378;

///////////////////// SupportingIon /////////////////////
///////////////////// SupportingIon /////////////////////
///////////////////// SupportingIon /////////////////////
///////////////////// SupportingIon /////////////////////
struct DECLSPEC SupportingIon
{
  int charge            = 0; // z = 1..3
  double monoisotopicMz = 0; // observed monoisotopic m/z
  double intensity      = 0; // observed intensity at monoisotopic peak
  // Keeping a pointer to the matching theoretical isotopic cluster
  // trace will enable the user of the deconvolver class to craft a
  // synthetic mass spectrum corresponding the matched observed
  // input mass spectrum signal.
  QSharedPointer<pappso::Trace> matchingIsotopicClusterQSPtr = nullptr;

  SupportingIon() {};

  SupportingIon(int charge,
                double monoisotopic_mz,
                double intensity,
                QSharedPointer<pappso::Trace> matching_isotopic_cluster_qsp)
    : charge(charge),
      monoisotopicMz(monoisotopic_mz),
      intensity(intensity),
      matchingIsotopicClusterQSPtr(matching_isotopic_cluster_qsp) {};

  SupportingIon(const SupportingIon &supporting_ion)
    : charge(supporting_ion.charge),
      monoisotopicMz(supporting_ion.monoisotopicMz),
      intensity(supporting_ion.intensity),
      matchingIsotopicClusterQSPtr(supporting_ion.matchingIsotopicClusterQSPtr)
  {
  }

  SupportingIon &
  operator=(const SupportingIon &supporting_ion)
  {
    if(&supporting_ion == this)
      return *this;

    charge                       = supporting_ion.charge;
    monoisotopicMz               = supporting_ion.monoisotopicMz;
    intensity                    = supporting_ion.intensity;
    matchingIsotopicClusterQSPtr = supporting_ion.matchingIsotopicClusterQSPtr;

    return *this;
  }

  QString
  toString() const
  {
    return QString(
             "Mono m/z: %1 - z: %2, intensity: %3, "
             "theoretical isotopic cluster:\n%4\n")
      .arg(monoisotopicMz, 0, 'f', 6)
      .arg(charge)
      .arg(intensity)
      .arg(matchingIsotopicClusterQSPtr->toString());
  }
};

///////////////////// IsoMatch /////////////////////
///////////////////// IsoMatch /////////////////////
///////////////////// IsoMatch /////////////////////
///////////////////// IsoMatch /////////////////////
// Structure describing a match between a theoretical isotopic cluster
// centroid and a centroid of the input centroided mass spectrum (observed)
struct DECLSPEC IsoMatch
{
  size_t theoreticalClusterIndex;
  size_t observedPeakIndex;
  double observedMz;
  double observedIntensity;

  QString
  toString() const
  {
    QString text;

    text += QString(
              "Theoretical cluster index: %1 - "
              "Observed peak index: %2 - "
              "Observed m/z: %3 - "
              "Observed intensity: %4\n")
              .arg(theoreticalClusterIndex)
              .arg(observedPeakIndex)
              .arg(observedMz, 0, 'f', 5)
              .arg(observedIntensity);

    return text;
  }
};

///////////////////// BinnedIsotopicCentroid /////////////////////
///////////////////// BinnedIsotopicCentroid /////////////////////
///////////////////// BinnedIsotopicCentroid /////////////////////
///////////////////// BinnedIsotopicCentroid /////////////////////
struct DECLSPEC BinnedIsotopicCentroid
{
  // 0 = first, ...
  // Attention, if working with heavy isotope labelled analytes,
  // O is not the monoisotopic centroid.
  int isotopeIndex;
  // Weighted centroid
  double neutralMass;
  // Summed intensity (in fact this is the summed probabilities)
  double intensity;

  QString
  toString() const
  {
    QString text;

    text += QString(
              "Isotopic centroid index: %1 - "
              "Neutral mass: %2 "
              "Intensity: %3\n")
              .arg(isotopeIndex)
              .arg(neutralMass, 0, 'f', 5)
              .arg(intensity);

    return text;
  }
};

///////////////////// FitQuality /////////////////////
///////////////////// FitQuality /////////////////////
///////////////////// FitQuality /////////////////////
///////////////////// FitQuality /////////////////////
// Structure to hold metric data about the matches
// that characterize an assignment.
struct DECLSPEC FitQuality
{
  // Pearson r (theoretical versus observed shape correlation)
  double pearsonCorrelation;
  // mean of isotopologue centroid spacing error in ppm
  double spacingErrorPpm;
  // fraction of observed intensity explained observed vs theoretical
  double explainedIntensity;
};

///////////////////// DeconvolutedFeature /////////////////////
///////////////////// DeconvolutedFeature /////////////////////
///////////////////// DeconvolutedFeature /////////////////////
///////////////////// DeconvolutedFeature /////////////////////
struct DECLSPEC DeconvolutedFeature
{
  double neutralMass = 0; // neutral monoisotopic mass
  double intensity   = 0; // summed monoisotopic intensity
                          // across all supporting charge states

  std::vector<SupportingIon> supportingIons;

  FitQuality fitQuality;

  DeconvolutedFeature(): neutralMass(0), intensity(0) {};

  DeconvolutedFeature(const DeconvolutedFeature &other)
    : neutralMass(other.neutralMass), intensity(other.intensity)
  {
    for(const SupportingIon &supp_ion : other.supportingIons)
      supportingIons.push_back({supp_ion.charge,
                                supp_ion.monoisotopicMz,
                                supp_ion.intensity,
                                supp_ion.matchingIsotopicClusterQSPtr});

    fitQuality.pearsonCorrelation = other.fitQuality.pearsonCorrelation;
    fitQuality.spacingErrorPpm    = other.fitQuality.spacingErrorPpm;
    fitQuality.explainedIntensity = other.fitQuality.explainedIntensity;
  }

  QString
  toString(bool with_supporting_ions = true) const
  {
    QString text = QString("Neutral mass: %1, intensity: %2\n")
                     .arg(neutralMass, 0, 'f', 6)
                     .arg(intensity);

    if(with_supporting_ions)
      {
        if(supportingIons.size() == 1)
          text += "\nOne supporting ion:\n";
        else
          text += QString("\n%1 Supporting ions:\n").arg(supportingIons.size());

        for(const SupportingIon &supp_ion : supportingIons)
          text += supp_ion.toString();
      }

    text += with_supporting_ions ? "" : "\n";

    text += QString(
              "Fit quality: correlation: %1 - spacing error (ppm): %2 - "
              "explained intensity error: %3")
              .arg(fitQuality.pearsonCorrelation)
              .arg(fitQuality.spacingErrorPpm)
              .arg(fitQuality.explainedIntensity);

    return text;
  }
};

///////////////////// ChargeSpecificFeature /////////////////////
///////////////////// ChargeSpecificFeature /////////////////////
///////////////////// ChargeSpecificFeature /////////////////////
///////////////////// ChargeSpecificFeature /////////////////////
struct DECLSPEC ChargeSpecificFeature
{
  int charge;
  double testedNeutralMass;
  double testedMz;
  double testedIntensity;
  double monoisotopicNeutralMass;
  double monoisotopicMz;
  double monoisotopicIntensity;
  QSharedPointer<pappso::Trace> matchingIsotopicClusterQSPtr = nullptr;

  FitQuality fitQuality;

  QString
  toString(bool with_fit_quality = false) const
  {
    QString text =
      QString(
        "charge: %1 - tested neutral mass: %2 - tested m/z: %3 - "
        "tested intensity: %4 - mono neutral mass: %5 - mono m/z: %6 "
        "- mono intensity: %7\n")
        .arg(charge)
        .arg(testedNeutralMass, 0, 'f', 5)
        .arg(testedMz, 0, 'f', 5)
        .arg(testedIntensity, 0, 'f', 0)
        .arg(monoisotopicNeutralMass, 0, 'f', 5)
        .arg(monoisotopicMz, 0, 'f', 5)
        .arg(monoisotopicIntensity);

    if(with_fit_quality)
      {
        text += "\nFit quality:\n";

        text += QString(
                  "Fit quality: correlation: %1 - spacing error (ppm): %2 - "
                  "explained intensity: %3\n")
                  .arg(fitQuality.pearsonCorrelation)
                  .arg(fitQuality.spacingErrorPpm)
                  .arg(fitQuality.explainedIntensity);
      }
    return text;
  }
};

/////////////////////// AbstractLowMassDeconvolver /////////////////////
/////////////////////// AbstractLowMassDeconvolver /////////////////////
/////////////////////// AbstractLowMassDeconvolver /////////////////////
/////////////////////// AbstractLowMassDeconvolver /////////////////////
/////////////////////// AbstractLowMassDeconvolver /////////////////////

// This class processes a centroided Trace and detects the isotopic clusters.
// For each detected cluster, it stores the monoisotopic m/z and the z.

class DECLSPEC AbstractLowMassDeconvolver
{

  public:
  struct DECLSPEC Parameters
  {
    // Charge settings
    int minCharge = 1;
    int maxCharge = 3;

    // Mass range (for low mass)
    double minMass = 200.0;  // Da
    double maxMass = 3500.0; // Da

    // Mass accuracy
    double ppmMassTolerance = 5.0; // tight tolerance for high-res
    // instruments

    // Intensity filtering
    double minIntensity = 100.0;

    // Min count of isotopic cluster matches (theoretical vs observed)
    int minIsotopicClusterMatches = 3;

    // Max count of isotopic cluster matches (theoretical vs observed)
    int maxIsotopicClusterMatches = 3;

    // The number of isotopic cluster centroids that are
    // used to compute the relative centroid intensity
    // so as to compare the observed relative intensities
    // with thoses of the theoretical cluster.
    int clusterShapeCentroidCount = 3;

    // The Averagine used for the isotopic cluster modelling.
    Averagine averagine;

    bool fullHeavyIsotopeLabelling = false;

    // If possible write to this file all the processing details.
    QString processingDetailsLogFilePath;

    // When a set of matches has occurred between a given theoretical cluster
    // and the corresponding matching input centroided mass spectrum centroids,
    // there might be a discrepancy between the sum of the intensities of the
    // theoretical cluster matched centroids and the intensities of the
    // corresponding input matched centroids. Of course the intensities are
    // normalized to the ratio between the intensity of the monoisotopic
    // (highest probability) theoretical cluster centroid and that of the
    // matched input centroid. The absolute difference between the summed
    // observed intensities
    // and the summed theoretical intensity divided by the summed theoretical
    // intensities reflect the proportion of observed or theoretical signal that
    // deviates from the other. If that proportion is beyond this parameter,
    // then there is too big a mismatch between the theoretical and the observed
    // clusters intensities.
    double totalClusterIntensityTolerance = 0.5;

    // When comparing the relative intensity of the observed vs theoretical
    // isotopic cluster centroids, two-by-tow (isotopologues 0, then
    // isotopologues 1), the following value is the tolerance that is accepted
    // for a deviation in the observed centroid intensity with respect to the
    // theoretical intensty. For example 0.6 would mean: "tolerate that the
    // intensity difference between observed and theoretical isotopologues at
    // position 0 of the cluster be up to (0.6 * theoretical intensity):
    // if (abs(obs_intensity - theo_intensity) > 0.6 * theoretical intensity ->
    // REJECT.
    double clusterShapeTolerance = 0.7;

    // Scoring threshold (optional)
    double minPearsonCorrelationScore = 0.3;

    Parameters();

    Parameters(int min_charge,
               int max_charge,
               double min_mass,
               double max_mass,
               double ppm_mass_tolerance,
               double min_intensity,
               int minIsotopicClusterMatches,
               int maxIsotopicClusterMatches,
               int clusterShapeCentroidCount,
               const Averagine &averagine,
               bool fullHeavyIsotopeLabelling,
               const QString &processing_details_log_file_path,
               double explained_intensity_threshold,
               double relative_cluster_shape_tolerance,
               double min_pearson_correlation_score);

    Parameters(const Parameters &other);

    Parameters &operator=(const Parameters &other);

    void initialize(int min_charge,
                    int max_charge,
                    double min_mass,
                    double max_mass,
                    double ppm_mass_tolerance,
                    double min_intensity,
                    int minIsotopicClusterMatches,
                    int maxIsotopicClusterMatches,
                    int clusterShapeCentroidCount,
                    const Averagine &averagine,
                    bool full_heavy_isotope_labelling,
                    const QString &processing_details_log_file_path,
                    double explained_intensity_threshold,
                    int relative_cluster_shape_tolerance,
                    double min_pearson_correlation_score);

    QString toString() const;
  };

  explicit AbstractLowMassDeconvolver(Parameters &parameters);

  virtual ~AbstractLowMassDeconvolver();

  virtual std::vector<DeconvolutedFeature>
  deconvolute(const pappso::Trace &centroided_mass_spectrum) const = 0;

  protected:
  Parameters m_params;
  QFile *mp_logFile             = nullptr;
  QTextStream *mp_logTextStream = nullptr;

  inline void
  logProcessingDetails(const QString &details_string) const
  {
    if(mp_logTextStream != nullptr)
      {
        *mp_logTextStream << details_string;
        // mp_logTextStream->flush();
      }
  }

  inline double
  neutralToMz(double neutral_mass, int charge) const
  {
    return (neutral_mass + charge * PROTON_MASS) / charge;
  }

  inline double
  mzToNeutral(double mz, int charge) const
  {
    return mz * charge - charge * PROTON_MASS;
  }

  inline double
  ppmToDelta(double mass_or_mz, double ppm) const
  {
    return (mass_or_mz * ppm) / 1e6;
  }

  inline double
  deltaToPpm(double delta_mass_or_mz, double mass_or_mz) const
  {
    // ppm = (|observed - theoretical| / theoretical) × 1,000,000

    double ppm_value = (delta_mass_or_mz / mass_or_mz) * 1e6;

    // qDebug() << "Got delta_mass_or_mz:" << delta_mass_or_mz
    //          << "and reference mz:" << mass_or_mz
    //          << "now returning ppm_value:" << ppm_value;

    return ppm_value;
  }

  inline bool
  neutralMassesMatch(double m1, double m2) const
  {
    return std::abs(m1 - m2) <= m_params.ppmMassTolerance;
  }

  pappso::Trace isotopicClusterForFormula(const QString &formula,
                                          double intensity) const;

  pappso::Trace isotopicClusterForNeutralMass(double neutral_mass,
                                              double intensity,
                                              QString &message) const;

  pappso::Trace collapseTheoreticalIsotopicCluster(
    const pappso::Trace &theoretical_cluster) const;

  pappso::Trace
  selectCandidatePeaks(const pappso::Trace &input_centroids) const;

  virtual bool fitIsotopicEnvelope(const pappso::Trace &input_centroids,
                                   std::size_t input_centroid_index,
                                   ChargeSpecificFeature &feature,
                                   std::vector<bool> &globally_used) const = 0;

  virtual std::vector<ChargeSpecificFeature> generateChargeSpecificFeatures(
    const pappso::Trace &input_centroids) const = 0;

  std::vector<DeconvolutedFeature> groupByNeutralMass(
    const std::vector<ChargeSpecificFeature> &charge_specific_features) const;

  virtual std::vector<IsoMatch>::const_iterator
  getMonoisotopicMatchIterator(const std::vector<IsoMatch> &matches,
                               QString &message) const = 0;

  virtual std::vector<IsoMatch>::const_iterator
  getIsotopologueMatchIterator(const std::vector<IsoMatch> &matches,
                               int isotopologue,
                               QString &message) const = 0;

  virtual pappso::Trace getIsotopicClusterReducedToMaxMatchCount(
    const pappso::Trace &isotopic_cluster) const = 0;

  pappso::Trace convertFromNeutralToChargedIsotopicCluster(
    const pappso::Trace &neutral_isotopic_cluster, int charge) const;
};

} // namespace libXpertMassCore
} // namespace MsXpS
