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


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


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


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


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

namespace MsXpS
{
namespace libXpertMassCore
{
namespace Processing
{

// clang-format off
/*!
\enum MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::IntensityThresholdCalculation
\ingroup XpertMassCoreProcessing

This enum type specifies the method to be used to caculate the maximum intensity
above which mass spectral data points are skipped in the local noise-to-signal
estimation.

\value MANUAL_BY_VALUE
The maximum intensity is defined manually.

\value AUTO_MAX_BY_STDEV
The maximum intensity will be determined using the standard deviation.

\value AUTO_MAX_BY_PERCENTILE
The maximum intensity will be determined using the percentile.
*/
// clang-format on


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

\brief The Parameters struct encapsulates all the parameters required to
configure a LocalSignalToNoiseEstimator.
*/
// clang-format on

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::Parameters::maxIntensity

\brief Maximal intensity above which peaks are discarded.

This value is important in order to avoid using data points of absurdly high
intensity (electronic spikes, for example). This value is used when the
\l{maxIntMode} value is set to
\l{LocalSignalToNoiseEstimator::IntensityThresholdCalculation}::MANUAL_BY_VALUE.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::Parameters::maxIntStdDevFactor

\brief Parameter used for the initial calculation of the maximum intensity above
which data points are skipped in the local signal-to-noise estimation. This
value is used when the
\l{maxIntMode} value is set to
\l{LocalSignalToNoiseEstimator::IntensityThresholdCalculation}::AUTO_MAX_BY_STDEV,
which is the default.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::Parameters::maxIntPercentile

\brief Parameter used for the initial calculation of the maximum intensity above
which data points are skipped in the local signal-to-noise estimation. This
value is used when the \l{maxIntMode} value is set to
\l{LocalSignalToNoiseEstimator::IntensityThresholdCalculation}::AUTO_MAX_BY_PERCENTILE.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::Parameters::maxIntMode

\brief Defines the method to be used to determine the maximum intensity that
data points may have. Above that intensity, data points are skipped for the
local signal-to-noise estimation.

\sa
MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::IntensityThresholdCalculation
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::Parameters::windowSize

\brief The size (in m/z unit) of the mass spectrum window that is comprised in
the local signal-to-noise estimation.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::Parameters::binCount

\brief The number of bins to be set to the histogram used for the local
signal-to-noise estimation.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::Parameters::minRequiredElements

\brief The minimum number of elements that must be found in the mass spectrum
window that is comprised in the local signal-to-noise estimation.

\sa windowSize
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::Parameters::noiseValueForEmptyWindow

\brief Value used as noise value for windows which cover less than
\l{minRequiredElements}.

Use a very hight value, like std::pow(10.0, 20), to get a very low S/N result.
*/

// clang-format off
/*!
\fn MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::Parameters::Parameters(double max_intensity,
               double max_int_std_dev_factor,
               double max_int_percentile,
               int max_int_mode,
               double window_size,
               int bin_count,
               int min_required_elements,
               double noise_value_for_empty_window)

\brief Constructs a Parameters instance with all the parameters for its initialization.

\list

\li \a max_intensity: initializes maxIntensity
\li \a max_int_std_dev_factor: initializes maxIntStdDevFactor
\li \a max_int_percentile: initializes maxIntPercentile
\li \a max_int_mode: initializes maxIntMode
\li \a window_size: initializes windowSize
\li \a bin_count: initializes binCount
\li \a min_required_elements: initializes minRequiredElements
\li \a noise_value_for_empty_window: initializes noiseValueForEmptyWindow

\endlist
*/
// clang-format on

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

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

// clang-format off
/*!
\fn MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::Parameters::operator=(const Parameters &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::LocalSignalToNoiseEstimator::Parameters::toString() const

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


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

\brief The GaussianEstimateParams struct encapsulates the characteristics of the
Gaussian as returned by \l{LocalSignalToNoiseEstimator::estimateGaussian}.
*/
// clang-format on

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::GaussianEstimateParams::mean

\brief The mean of the estimated Gaussian, that is the apex.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::GaussianEstimateParams::variance

\brief The variance of the estimated Gaussian (σ, that is, sigma).

This value is the square of the standard deviation. The variance relates to the
FWHM value by this relation:

\code
FWHM = 2 * sqrt(2 * Ln 2​) * sqrt(σ^2)​= 2 * sqrt(2 * Ln 2)​⋅σ
\endcode
*/

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

\brief The LocalSignalToNoiseEstimator class encapsulates all the features
required to compute a signal-to-noise ratio value for each point in a trace.

This class is used in support of the code that performs the low mass
deconvolution of an input mass spectrum.
*/

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

\brief The Parameters required to perform the local signal-to-noise estimation.
*/


/*!
\variable \
MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::m_sparseWindowPercentage

\brief Counter for the number of sparse windows.

This value is updated while the local signal-to-noise estimation is carried out.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::m_histogramOverflowPercentage

\brief Counter of histogram overflow events.

This value is updated while the local signal-to-noise estimation is carried out.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LocalSignalToNoiseEstimator::m_signalToNoiseEstimates

\brief The actual calculated local signal-to-noise estimates.

Each point in the input mass spectrum has a corresponding double value in this
container. The user of this class will be able to query the noise estimation
corresponding to any mass spectral data point of the input mass spectrum.
*/

/*!
\brief Constructs a LocalSignalToNoiseEstimator instance with default
parameters.
*/
LocalSignalToNoiseEstimator::LocalSignalToNoiseEstimator()
  : m_parameters(LocalSignalToNoiseEstimator::Parameters())
{
}

/*!
\brief Constructs a LocalSignalToNoiseEstimator instance with \a parameters to
initialize m_parameters.
*/
LocalSignalToNoiseEstimator::LocalSignalToNoiseEstimator(const Parameters &parameters)
  : m_parameters(parameters)
{
}

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

/*!
\brief Estimates the Gaussian for the mass spectral points contained in the
iterator range [\a iter_first_data_point - \a iter_last_data_point].

Returns the Gaussian parameters as a GaussianEstimateParams object.
*/
LocalSignalToNoiseEstimator::GaussianEstimateParams
LocalSignalToNoiseEstimator::estimateGaussian(
  const TraceIterator &iter_first_data_point,
  const TraceIterator &iter_last_data_point) const
{
  int size          = 0;
  // add up
  double v          = 0;
  double m          = 0;
  TraceIterator run = iter_first_data_point;
  while(run != iter_last_data_point)
    {
      m += (*run).y;
      ++size;
      ++run;
    }
  // average
  m = m / size;

  // determine variance
  run = iter_first_data_point;
  while(run != iter_last_data_point)
    {
      double tmp(m - (*run).y);
      v += tmp * tmp;
      ++run;
    }
  v = v / ((double)size); // divide by n

  GaussianEstimateParams estimate_params = {m, v};
  return estimate_params;
}

/*!
\brief Initializes this LocalSignalToNoiseEstimator instance by looking into \a
trace.

The input trace is not stored in this instance. It is iterated into and for each
one of its pappso::DataPoint instances the local signal-to-noise ratio is
estimated and stored in m_signalToNoiseEstimates.

Calls \l{computeSignaToNoiseRatio()}
*/
void
LocalSignalToNoiseEstimator::initialize(const pappso::Trace &trace)
{
  computeSignaToNoiseRatio(trace);
}

/*!
\brief Iterates in \a trace and, for each one of its pappso::DataPoint
instances, estimates the local signal-to-noise ratio by using a sliding window
approach.

\sa initialize()
*/
void
LocalSignalToNoiseEstimator::computeSignaToNoiseRatio(
  const pappso::Trace &trace)
{
  // First element in the pappso::Trace
  TraceIterator iter_first_data_point = trace.begin();
  // Last element in the pappso::Trace
  TraceIterator iter_last_data_point  = trace.end();

  // Reset counter for sparse windows
  m_sparseWindowPercentage      = 0;
  // Reset counter for histogram overflow
  m_histogramOverflowPercentage = 0;

  // Reset the results
  m_signalToNoiseEstimates.clear();
  m_signalToNoiseEstimates.resize(trace.size());

  // Maximal range of histogram needs to be calculated first
  if(m_parameters.maxIntMode == AUTO_MAX_BY_STDEV)
    {
      // Use MEAN+auto_m_parameters.maxIntensity*STDEV as threshold
      GaussianEstimateParams gaussian_estimate_global =
        estimateGaussian(iter_first_data_point, iter_last_data_point);
      m_parameters.maxIntensity =
        gaussian_estimate_global.mean +
        std::sqrt(gaussian_estimate_global.variance) * m_parameters.maxIntStdDevFactor;

      // qDebug() << "Computed max intensity:" << m_parameters.maxIntensity;
    }
  else if(m_parameters.maxIntMode == AUTO_MAX_BY_PERCENTILE)
    {
      // get value at "m_parameters.maxIntPercentile"th percentile
      // we use a histogram approach here as well.
      if((m_parameters.maxIntPercentile < 0) || (m_parameters.maxIntPercentile > 100))
        {
          qFatal() << "The m_parameters.maxIntPercentile value is not in the [0, 100] range:"
                   << m_parameters.maxIntPercentile;
        }

      // Histogram with 100 percent values all initialized to 0.
      QList<int> histogram_of_intensity_distribution(100, 0);

      // Find maximum of current scan
      auto max_intensity_data_point_iterator = std::max_element(
        trace.begin(),
        trace.end(),
        [](const pappso::DataPoint &a, const pappso::DataPoint &b) {
          return a.y > b.y;
        });

      double max_intensity = max_intensity_data_point_iterator->y;

      // qDebug() << "Max intensity value in the trace:" << max_intensity;

      double histogram_bin_size = max_intensity / 100;

      // Fill histogram
      for(const pappso::DataPoint &data_point : trace)
        {
          // Increment count at histogram bin that is the bin corresponding
          // to the iterated data point intensity in the percentage scale above.
          int bin = (int)((data_point.y - 1) / histogram_bin_size);
          ++histogram_of_intensity_distribution[bin];
        }

      // Add up element counts in histogram until ?th percentile is reached
      // For example if percentile is 95 percent, then the value elements_below_max_int_percentile
      // below is the number of data points that correspond to 95 % of the whole number of data
      // points in the spectrum.
      int elements_below_max_int_percentile =
        (int)(m_parameters.maxIntPercentile * trace.size() / 100);

      int elements_seen = 0;
      int i             = -1;

      TraceIterator the_iterator = iter_first_data_point;

      // This loop is equivalent to iterating in the bins of the histogram,
      // and for each bin sum the corresponding count to elements_seen.
      // When elements_seen reaches 95% of the number of DataPoint instances
      // in the mass spectrum, stop and record the bin number (i).
      while(the_iterator != iter_last_data_point &&
            elements_seen < elements_below_max_int_percentile)
        {
          ++i;
          elements_seen += histogram_of_intensity_distribution[i];
          ++the_iterator;
        }

      // Because each histogram_bin_size accounts for a hundredth of the max intensity of the
      // whole mass spectra data, returning the i * histogram_bin_size is equivalent to
      // returning the greatest intensity value encountered in the 95% of the mass spectrum.
      // Half a bin is added to center the returned value in the bin.
      m_parameters.maxIntensity = (((double)i) + 0.5) * histogram_bin_size;

      // qDebug() << "Computed max intensity:" << m_parameters.maxIntensity;
    }
  else // if (m_parameters.maxIntMode == MANUAL_BY_VALUE)
    {
      if(m_parameters.maxIntensity <= 0)
        {
          qFatal() << "The method to determine the maximal intensity is set to MANUAL"
                   << "However, the m_parameters.maxIntensity value is <= 0.";
        }

      // qDebug() << "The max intensity was set manually:" << m_parameters.maxIntensity;
    }

  if(m_parameters.maxIntensity < 0)
    {
      qFatal() << "The m_parameters.maxIntensity "
                  "value should be positive! ";
    }

  TraceIterator window_pos_center       = iter_first_data_point;
  TraceIterator window_pos_border_left  = iter_first_data_point;
  TraceIterator window_pos_border_right = iter_first_data_point;

  double window_half_size = m_parameters.windowSize / 2;
  // At least size of 1 for intensity bins
  double bin_size         = std::max(1.0, m_parameters.maxIntensity / m_parameters.binCount);
  int bin_count_minus_one = m_parameters.binCount - 1;

  // Allocate the histogram with the user-configured number of bins
  std::vector<int> histogram(m_parameters.binCount, 0);

  // This vector will store for each bin, the value in it.
  std::vector<double> bin_values(m_parameters.binCount, 0);

  // Calculate average intensity that is represented by a bin
  for(int bin = 0; bin < m_parameters.binCount; bin++)
    {
      histogram[bin] = 0;
      bin_values[bin] = (bin + 0.5) * bin_size;
    }

  // This will store the bin in which a datapoint would fall.
  int to_bin = 0;

  // index of bin where the median is located
  int median_bin        = 0;
  // additive number of elements from left to x in histogram
  int element_inc_count = 0;

  // tracks elements in current window, which may vary because of unevenly
  // spaced data
  int elements_in_window = 0;
  // number of windows
  int window_count       = 0;

  // number of elements where we find the median
  int element_in_window_half = 0;

  double noise; // noise value of a datapoint

  // Iterate in the mass spectrum and at each data point, determine the signal-to-noise ratio
  // and store it. The signal-to-noise ratio is computed over a rolling window of
  // a configured size that is centered on the iterated data point.
  // At each data point in the mass spectrum (that is, at
  // iterator window_pos_center), update the histogram data for the rolling window.
  // The data in the histogram will serve as the basis to determine the noise.

  while(window_pos_center != iter_last_data_point)
    {
      // Each time we iterate in a new mass spectrum DataPoint, that is, each
      // time we increment window_pos_center, we need to shift the rolling window
      // to center it at the new mass spectrum data point.

      // Erase all elements from histogram that will leave the rolling window on the
      // LEFT side
      while((*window_pos_border_left).x <
            (*window_pos_center).x - window_half_size)
        {
          to_bin = std::max(
            std::min<int>((int)((*window_pos_border_left).y / bin_size),
                          bin_count_minus_one),
            0);

          // Remove the current window's left half's DataPoint from
          // the histogram counts at bin corresponding to its intensity.
          --histogram[to_bin];
          // Remove the current window's left half's DataPoint from
          // the total count of DataPoint instances in the rolling window.
          --elements_in_window;
          // Now go on and shift the rolling window's left border iterator one position
          // to the right.
          ++window_pos_border_left;

          // Do this until the iterator of the left border of the rolling window points
          // to a mass spectrum DataPoint that has its m/z value matching the
          // current rolling window center iterator m/z value minus half of the
          // rolling window width.
        }

      // Add all elements to histogram that will enter the rolling window on the RIGHT
      // side
      while((window_pos_border_right != iter_last_data_point) &&
            ((*window_pos_border_right).x <=
             (*window_pos_center).x + window_half_size))
        {
          // std::cerr << (*window_pos_border_right).y << " " <<
          // bin_size << " " << m_parameters.binCountminus_1 << std::endl;
          to_bin = std::max(
            std::min<int>((int)((*window_pos_border_right).y / bin_size),
                          bin_count_minus_one),
            0);
          ++histogram[to_bin];
          ++elements_in_window;
          ++window_pos_border_right;
        }

      if(elements_in_window < m_parameters.minRequiredElements)
        {
          noise = m_parameters.noiseValueForEmptyWindow;
          ++m_sparseWindowPercentage;
        }
      else
        {
          // Find bin i where ceil[elements_in_window/2] <= sum_c(0..i){
          // histogram[c] }
          median_bin             = -1;
          element_inc_count      = 0;
          element_in_window_half = (elements_in_window + 1) / 2;
          while(median_bin < bin_count_minus_one &&
                element_inc_count < element_in_window_half)
            {
              ++median_bin;
              element_inc_count += histogram[median_bin];
            }

          // Increase the error count
          if(median_bin == bin_count_minus_one)
            {
              ++m_histogramOverflowPercentage;
            }

          // Just avoid division by 0
          noise = std::max(1.0, bin_values[median_bin]);
        }

      // Store result
      m_signalToNoiseEstimates[window_count] = (*window_pos_center).y / noise;
      // qDebug() << "Added new signal to noise estimate:"
      //          << m_signalToNoiseEstimates.at(window_count);

      // Advance the window center by one datapoint
      ++window_pos_center;
      ++window_count;

    } // end while

  // qDebug() << "Done filling in the signal to noise estimates, the are"
  //          << m_signalToNoiseEstimates.size() << "estimates.";

  m_sparseWindowPercentage = m_sparseWindowPercentage * 100 / window_count;
  m_histogramOverflowPercentage =
    m_histogramOverflowPercentage * 100 / window_count;

  // warn if percentage of sparse windows is above 20%
  if(m_sparseWindowPercentage > 20)
    {
      qWarning() << m_sparseWindowPercentage
                 << "% of all windows were sparse. You should consider increasing"
                 << "'m_parameters.windowSize' or decrease 'm_parameters.minRequiredElements'";
    }

  // warn if percentage of possibly wrong median estimates is above 1%
  if(m_histogramOverflowPercentage > 1)
    {
      qWarning() << m_histogramOverflowPercentage
                 << "% of all Signal-to-Noise estimates are too high, because the "
                    "median was found in the rightmost histogram-bin. "
                 << "You should consider increasing 'm_parameters.maxIntensityy' (and maybe "
                    "'m_parameters.binCount' with it, to keep bin width reasonable)";
    }
}

/*!
\brief Returns the item at \a index in the member container
m_signalToNoiseEstimates of local signal-to-noise ratios.

Each data point in the input mass spectrum trace used to initialize this
LocalSignalToNoiseEstimator instance has a corresponding (that is, at the same
\a index) double value representing its local signal-to-noise estimated value.
*/
double
LocalSignalToNoiseEstimator::getSignalToNoiseRatio(qsizetype index) const
{
  Q_ASSERT(index < m_signalToNoiseEstimates.size());
  return m_signalToNoiseEstimates[index];
}


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