// Copyright 2026 Filippo Rusconi
// Inspired by work by Lars Nilse in OpenMS


/////////////////////// stdlib includes
#include <limits>


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


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


/////////////////////// Local includes
#include "MsXpS/libXpertMassCore/Processing/HighResPeakPicker.hpp"
#include "MsXpS/libXpertMassCore/Processing/CubicSplineModel.hpp"
#include "MsXpS/libXpertMassCore/Processing/LocalSignalToNoiseEstimator.hpp"

namespace MsXpS
{
namespace libXpertMassCore
{
namespace Processing
{

/*!
\struct MsXpS::libXpertMassCore::Processing::HighResPeakPickerParameters
\inmodule libXpertMassCore
\ingroup XpertMassCoreProcessing
\inheaderfile MsXpS/libXpertMassCore/HighResPeakPicker.hpp

\brief Provides parameters for the \l HighResPeakPicker peak picking feature.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::HighResPeakPickerParameters::signalToNoise

\brief Signal to noise ratio above which the peak picking process is aborted.

This value is used to filter centroids on the basis of the
signal to noise estimation computed by \l LocalSignalToNoiseEstimator.

Setting this value to 0 removes the estimation of the signal to noise
entirely.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::HighResPeakPickerParameters::missingPeakCount

\brief Number of actual peaks admitted to be absent from the spline model.

 */

/*!
\variable \
MsXpS::libXpertMassCore::Processing::HighResPeakPickerParameters::minimumIntensity

\brief Skip any data point from the input mass spectrum that has an intensity
below this value.
 */


// clang-format off
/*!
\fn MsXpS::libXpertMassCore::Processing::HighResPeakPickerParameters::HighResPeakPickerParameters()

\brief Constructs a HighResPeakPickerParameters instance using defaults for
members.
*/
// clang-format on

// clang-format off
/*!
\fn MsXpS::libXpertMassCore::Processing::HighResPeakPickerParameters::HighResPeakPickerParameters(const HighResPeakPickerParameters &other)

\brief Constructs a HighResPeakPickerParameters instance using \a other as a template.
*/
// clang-format on


// clang-format off
/*!
\fn MsXpS::libXpertMassCore::Processing::HighResPeakPickerParameters::operator=(const HighResPeakPickerParameters &other)

\brief Assigns \a other to this instance and returns a reference to this instance.
*/
// clang-format on

// clang-format off
/*!
\fn MsXpS::libXpertMassCore::Processing::HighResPeakPickerParameters::toString() const

\brief Crafts a string documenting this instance and returns it.
*/
// clang-format on

/*!
\struct MsXpS::libXpertMassCore::Processing::HighResPeakPicker::PeakRange
\inmodule libXpertMassCore
\ingroup XpertMassCoreProcessing
\inheaderfile MsXpS/libXpertMassCore/HighResPeakPicker.hpp

\brief Provides start and end points for peaks.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::HighResPeakPicker::PeakRange::mzStart

\brief Start m/z value of the peak.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::HighResPeakPicker::PeakRange::mzStop

\brief Stop m/z value of the peak.
*/

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

\brief Provides a peak picking feature for data acquired on high resolving power
instruments.

Peak picking is also known as centroidation, where the software extracts from
a profile mode shaped peak the m/z value that best represents it.

The process that is carried out in this picker is based on the modeling of the
shaped peak profile using a spline model and on the determination of the
centroid value out of it.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::HighResPeakPicker::m_parameters

\brief The parameters required to configure the peak picker.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::HighResPeakPicker::m_localSignalToNoiseEstimatorParams

\brief The local signal-to-noise estimator parameters.
*/

/*!
\brief Constructs a HighResPeakPicker instance with default member values.
*/
HighResPeakPicker::HighResPeakPicker()
  : m_parameters(HighResPeakPickerParameters())
{
}

/*!
\brief Constructs a HighResPeakPicker instance with \a parameters and \a
signal_to_noise_estimator_params.
*/
HighResPeakPicker::HighResPeakPicker(
  HighResPeakPickerParameters &parameters,
  LocalSignalToNoiseEstimator::Parameters signal_to_noise_estimator_params)
  : m_parameters(parameters),
    m_localSignalToNoiseEstimatorParams(signal_to_noise_estimator_params)
{
}

/*!
\brief Destructs this instance.
*/
HighResPeakPicker::~HighResPeakPicker()
{
}

/*!
\brief Sets the member local signal-to-noise estimator parameters to \a
parameters.
*/
void
HighResPeakPicker::setLocalSignalToNoiseEstimatorParams(
  const LocalSignalToNoiseEstimator::Parameters &parameters)
{
  m_localSignalToNoiseEstimatorParams = parameters;
}

/*!
\brief Assigns \a other to this instance and returns a reference to this
instance.
*/
HighResPeakPicker &
HighResPeakPicker::operator=(const HighResPeakPicker &other)
{
  if(&other == this)
    return *this;

  m_parameters = other.m_parameters;
  m_localSignalToNoiseEstimatorParams =
    other.m_localSignalToNoiseEstimatorParams;

  return *this;
}

/*!
\brief Runs the peak picking process on \a initial_trace.

In the process of peaking peaks \a picked_peaks is updated with found peaks and
\a peak_ranges also.

For each found peak added to \a picked_peaks, a corresponding PeakRange item is
added to \a peak_ranges. This allows to enquire about the range of any single
peak in \a picked_peaks.
*/
void
HighResPeakPicker::pick(const pappso::Trace &initial_trace,
                        pappso::Trace &picked_peaks,
                        QList<PeakRange> &peak_ranges)
{
  picked_peaks.clear();

  qsizetype trace_size = initial_trace.size();

  if(trace_size < 5)
    return;

  // signal-to-noise estimation. IF the params were not set the
  // default ones in the object itself are reasonable.
  LocalSignalToNoiseEstimator local_signal_estimator(
    m_localSignalToNoiseEstimatorParams);

  if(m_parameters.signalToNoise > 0.0)
    {
      // Computes, for each DataPoint in the trace, an estimage of local
      // signal-to-ratio value that is consulted below.
      local_signal_estimator.initialize(initial_trace);
    }

  // Iterate in the trace and search for local maxima, that is, roughly,
  // DataPoint instances that are higher than previous and next point.
  for(qsizetype iter = 2; iter < trace_size - 2; ++iter)
    {
      // Skip any data point that is of an intensity below min_intensity
      if(initial_trace.at(iter).y < m_parameters.minimumIntensity)
        {
          // qDebug() << "Now skipping low intensity peak at m/z:" << trace.at(iter).x
          //          << "with intensity:" << trace.at(iter).y;
          continue;
        }
      double center_data_point_mz  = initial_trace.at(iter).x;
      double center_data_point_int = initial_trace.at(iter).y;
      double left_data_point_mz    = initial_trace.at(iter - 1).x;
      double left_data_point_int   = initial_trace.at(iter - 1).y;
      double right_data_point_mz   = initial_trace.at(iter + 1).x;
      double right_data_point_int  = initial_trace.at(iter + 1).y;

      // Do not interpolate when the left or right DataPoint is zero-intensity.
      if(std::fabs(left_data_point_int) <
         std::numeric_limits<double>::epsilon())
        {
          continue;
        }
      if(std::fabs(right_data_point_int) <
         std::numeric_limits<double>::epsilon())
        {
          continue;
        }

      double current_snt_estimate         = 0.0;
      double current_snt_estimate_left_1  = 0.0;
      double current_snt_estimate_right_1 = 0.0;

      if(m_parameters.signalToNoise > 0.0)
        {
          current_snt_estimate =
            local_signal_estimator.getSignalToNoiseRatio(iter);

          // qDebug() << "For current center point at iter:" << iter
          //          << "with mz:" << center_data_point_mz << "of intensity"
          //          << center_data_point_int
          //          << "the snt estimate is:" << current_snt_estimate;

          current_snt_estimate_left_1 =
            local_signal_estimator.getSignalToNoiseRatio(iter - 1);
          current_snt_estimate_right_1 =
            local_signal_estimator.getSignalToNoiseRatio(iter + 1);
        }

      // Look for "peaking" data points meeting MZ and intensity/SNT criteria
      // The currently iterated data point should at least have its
      // intensity greater than both the left and right data points intensities.
      // If the signal-to-noise checks are OK, than the data point becomes a peak "core",
      // that is the top point of the peak shape.
      if((center_data_point_int > left_data_point_int) &&
         (center_data_point_int > right_data_point_int) &&
         (current_snt_estimate >= m_parameters.signalToNoise) &&
         (current_snt_estimate_left_1 >= m_parameters.signalToNoise) &&
         (current_snt_estimate_right_1 >= m_parameters.signalToNoise))
        {
          // First check if the currently iterated center center point
          // is surrounded by more intense peaks on its left-left or on its
          // right-right, because that might indicate signal instability instead
          // of an actual maximum. In this case, we do not consider the data
          // point.

          double current_snt_estimate_left_2  = 0.0;
          double current_snt_estimate_right_2 = 0.0;

          if(m_parameters.signalToNoise > 0.0)
            {
              current_snt_estimate_left_2 =
                local_signal_estimator.getSignalToNoiseRatio(iter - 2);
              current_snt_estimate_right_2 =
                local_signal_estimator.getSignalToNoiseRatio(iter + 2);
            }

          // Checking signal-to-noise
          if((iter > 1) &&
             (iter + 2 < static_cast<qsizetype>(initial_trace.size())) &&
             (left_data_point_int < initial_trace.at(iter - 2).y) &&
             (right_data_point_int < initial_trace.at(iter + 2).y) &&
             (current_snt_estimate_left_2 >= m_parameters.signalToNoise) &&
             (current_snt_estimate_right_2 >= m_parameters.signalToNoise))
            {
              // If the point left of the left data point (with respect to
              // current center data point that was found to be a local maximum)
              // and if the point right of the right data point (again, with
              // respect to current center data point that was found to be a
              // local maximum) and both of these left_left and right_right
              // datapoints are above the signal to noise ratio, then we are
              // experiencing signal instability.
              ++iter;
              continue;
            }

          QMap<double, double> peak_raw_data;

          peak_raw_data[center_data_point_mz] = center_data_point_int;
          peak_raw_data[left_data_point_mz]   = left_data_point_int;
          peak_raw_data[right_data_point_mz]  = right_data_point_int;

          // Peak core found, now extend it to the left
          qsizetype k = 2;

          // No need to extend peak if previous intensity was zero
          bool previous_zero_left      = false;
          qsizetype missing_left_count = 0;
          // Index of the left boundary for the spline interpolation
          qsizetype left_boundary      = iter - 1;

          while((k <= iter) && // Prevent underflow
                (iter - k + 1 > 0) && !previous_zero_left &&
                (initial_trace.at(iter - k).y <= peak_raw_data.begin().value()))
            {
              double current_snt_estimate_left_k = 0.0;

              if(m_parameters.signalToNoise > 0.0)
                {
                  current_snt_estimate_left_k =
                    local_signal_estimator.getSignalToNoiseRatio(iter - k);
                }

              if(current_snt_estimate_left_k >= m_parameters.signalToNoise)
                {
                  peak_raw_data[initial_trace.at(iter - k).x] =
                    initial_trace.at(iter - k).y;
                }
              {
                ++missing_left_count;
                if(missing_left_count <= m_parameters.missingPeakCount)
                  {
                    peak_raw_data[initial_trace.at(iter - k).x] =
                      initial_trace.at(iter - k).y;
                  }
              }

              previous_zero_left = (initial_trace.at(iter - k).y == 0);
              left_boundary      = iter - k;
              ++k;
            }

          // Now do the same on the right
          k = 2;

          // No need to extend peak if previous intensity was zero
          bool previous_zero_right      = false;
          qsizetype missing_right_count = 0;
          // Index of the right boundary for the spline interpolation
          qsizetype right_boundary(iter + 1);

          while((iter + k < static_cast<qsizetype>(initial_trace.size())) &&
                !previous_zero_right &&
                (initial_trace.at(iter + k).y <= peak_raw_data.last()))
            {
              double current_snt_estimate_right_k = 0.0;

              if(m_parameters.signalToNoise > 0.0)
                {
                  current_snt_estimate_right_k =
                    local_signal_estimator.getSignalToNoiseRatio(iter + k);
                }

              if(current_snt_estimate_right_k >= m_parameters.signalToNoise)
                {
                  peak_raw_data[initial_trace.at(iter + k).x] =
                    initial_trace.at(iter + k).y;
                }
              else
                {
                  ++missing_right_count;
                  if(missing_right_count <= m_parameters.missingPeakCount)
                    {
                      peak_raw_data[initial_trace.at(iter + k).x] =
                        initial_trace.at(iter + k).y;
                    }
                }

              previous_zero_right = (initial_trace.at(iter + k).y == 0);
              right_boundary      = iter + k;
              ++k;
            }

          // Skip peak altogether if the minimal number of 3 points for fitting is not reached
          if(peak_raw_data.size() < 3)
            {
              continue;
            }

          // qDebug() << "peak_raw_data size:" << peak_raw_data.size();

          CubicSplineModel cubic_spline_model(peak_raw_data);
          // qDebug() << "Stack-allocated:" << &cubic_spline_model;


          // qDebug() << "The cubic spline model has "
          //          << cubic_spline_model.getKnots().size() << "knots.";

          // calculate maximum by evaluating the spline's 1st derivative
          // (bisection method)
          double max_peak_mz  = center_data_point_mz;
          double max_peak_int = center_data_point_int;
          double threshold    = 1e-6;
          spline_bisection(cubic_spline_model,
                           left_data_point_mz,
                           right_data_point_mz,
                           max_peak_mz,
                           max_peak_int,
                           threshold);

          // save picked peak into output spectrum
          pappso::DataPoint data_point;

          PeakRange peak_range;

          data_point.x        = max_peak_mz;
          data_point.y        = max_peak_int;
          peak_range.mzStart  = initial_trace.at(left_boundary).x;
          peak_range.mzStop   = initial_trace.at(right_boundary).x;

          // qDebug() << "Now appending new data point:" <<
          // data_point.toString();

          picked_peaks.append(data_point);

          peak_ranges.push_back(peak_range);

          // jump over profile data points that have been considered already
          iter += k - 1;
        }
    }

  // qDebug() << "Now returning" << picked_peaks.size()
  //          << "picked centroids:" << picked_peaks.toString();
}


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