/* BEGIN software license
 *
 * msXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright(C) 2009,...,2018 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 <cmath>
#include <map>

/////////////////////// Qt includes
#include <QDateTime>
#include <QDebug>
#include <QFile>
#include <QString>
#include <QJSEngine>

/////////////////////// pappsomspp includes
#include "pappsomspp/core/massspectrum/massspectrum.h"
#include "pappsomspp/core/utils.h"

/////////////////////// Local includes
#include "pappsomspp/core/processing/combiners/mzintegrationparams.h"

/*
int mzIntegrationParamsMetaTypeId =
  qRegisterMetaType<pappso::MzIntegrationParams>("pappso::MzIntegrationParams");
int mzIntegrationParamsPtrMetaTypeId =
  qRegisterMetaType<pappso::MzIntegrationParams *>("pappso::MzIntegrationParams
*");*/

namespace pappso
{

//! Map relating the BinningType to a textual representation
std::map<MzIntegrationParams::BinningType, QString> binningTypeMap{
  {MzIntegrationParams::BinningType::NONE, "NONE"},
  {MzIntegrationParams::BinningType::DATA_BASED, "DATA_BASED"},
  {MzIntegrationParams::BinningType::ARBITRARY, "ARBITRARY"}};

MzIntegrationParams::BinningType
getBinningTypeFromString(const QString &text)
{
  std::map<MzIntegrationParams::MzIntegrationParams::BinningType,
           QString>::const_iterator the_iterator_const =
    std::find_if(
      binningTypeMap.begin(),
      binningTypeMap.end(),
      [text](const std::pair<MzIntegrationParams::BinningType, QString> &pair) {
        return pair.second == text;
      });

  if(the_iterator_const != binningTypeMap.end())
    return the_iterator_const->first;

  return MzIntegrationParams::BinningType::NONE;
}

MzIntegrationParams::MzIntegrationParams(QObject *parent): QObject(parent)
{
}

MzIntegrationParams::MzIntegrationParams(const QString &text, QObject *parent)
  : QObject(parent)
{
  initialize(text);
}

MzIntegrationParams::MzIntegrationParams(
  double min_mz,
  double max_mz,
  MzIntegrationParams::BinningType binning_type,
  pappso::PrecisionPtr bin_size_model,
  int bin_size_divisor,
  int decimal_places,
  bool remove_zero_val_data_points,
  QObject *parent)
  : QObject(parent),
    m_smallestMz(min_mz),
    m_greatestMz(max_mz),
    m_binningType(binning_type),
    m_binSizeModel(bin_size_model),
    m_binSizeDivisor(bin_size_divisor),
    m_decimalPlaces(decimal_places),
    m_removeZeroValDataPoints(remove_zero_val_data_points)
{
  if(m_binSizeModel == nullptr)
    m_binSizeModel = pappso::PrecisionFactory::getPpmInstance(20);

  // qDebug() << "MetaObject?" << this->metaObject()->className();
}

MzIntegrationParams::~MzIntegrationParams()
{
}

MzIntegrationParams *
MzIntegrationParams::clone(QObject *parent) const
{
  MzIntegrationParams *mz_integration_params_p =
    new MzIntegrationParams(m_smallestMz,
                            m_greatestMz,
                            m_binningType,
                            m_binSizeModel,
                            m_binSizeDivisor,
                            m_decimalPlaces,
                            m_removeZeroValDataPoints,
                            parent);

  return mz_integration_params_p;
}

MzIntegrationParams::InitializationResult
MzIntegrationParams::initialize(const QString &text)
{
  // qDebug() << "Initializing MzIntegrationParams using text:" << text;

  InitializationResult initialization_result = InitializationResult::DEFAULT;

  if(text.isEmpty())
    return initialization_result;

  reset();

  // Expected text: "Smallest (first) m/z:294.725158\nGreatest (last)
  // m/z:2055.002453\nDecimal places:-1\nBinning type:2\nBin size model:0.05
  // dalton\nBin size divisor:1\nRemove 0-val data points:1\n"

  // In order to consider that the parameters were correctly read, we need
  // some of the members to effectively be set from the settings values.

  bool binning_type_set            = false;
  bool decimal_places_set          = false;
  bool bin_size_model_set          = false;
  bool bin_size_divisor_set        = false;
  bool remove_zero_data_points_set = false;

  QStringList string_list = text.split("\n");

  for(int iter = 0; iter < string_list.size(); ++iter)
    {
      QString iter_string = string_list.at(iter);

      // qDebug() << "Iterating in string:" << iter_string;

      if(iter_string.contains("Binning type:"))
        {
          setBinningType(
            getBinningTypeFromString(iter_string.split(':').last()));
          binning_type_set = true;
        }
      else if(iter_string.contains("Bin size model:"))
        {
          setBinSizeModel(
            PrecisionFactory::fromString(iter_string.split(':').last()));
          bin_size_model_set = true;
        }
      else if(iter_string.contains("Bin size divisor:"))
        {
          setBinSizeDivisor(iter_string.split(':').last().toInt());
          bin_size_divisor_set = true;
        }
      else if(iter_string.contains("Decimal places:"))
        {
          setDecimalPlaces(iter_string.split(':').last().toInt());
          decimal_places_set = true;
        }
      else if(iter_string.contains("Remove 0-val data points:"))
        {
          setRemoveZeroValDataPoints(iter_string.split(':').last().toInt());
          remove_zero_data_points_set = true;
        }
    }

  // qDebug() << "At this point the initialization is done and this"
  //             " MzIntegrationParams instance is: "
  //          << toString();

  if(binning_type_set)
    initialization_result |= InitializationResult::BINNING_TYPE;
  if(bin_size_model_set)
    initialization_result |= InitializationResult::BIN_SIZE_MODEL;
  if(bin_size_divisor_set)
    initialization_result |= InitializationResult::BIN_SIZE_DIVISOR;
  if(decimal_places_set)
    initialization_result |= InitializationResult::DECIMAL_PLACES;

  if(remove_zero_data_points_set)
    initialization_result |= InitializationResult::REMOVE_ZERO_DATA_POINTS;

  return initialization_result;
}

void
MzIntegrationParams::initialize(double min_mz,
                                double max_mz,
                                MzIntegrationParams::BinningType binning_type,
                                pappso::PrecisionPtr bin_size_model,
                                int bin_size_divisor,
                                int decimal_places,
                                bool remove_zero_val_data_points,
                                QObject *parent)
{
  setSmallestMz(min_mz);
  setGreatestMz(max_mz);
  setBinningType(binning_type);
  setBinSizeModel(bin_size_model);
  setBinSizeDivisor(bin_size_divisor);
  setDecimalPlaces(decimal_places);
  setRemoveZeroValDataPoints(remove_zero_val_data_points);

  setParent(parent);
}

void
MzIntegrationParams::initialize(const MzIntegrationParams &other,
                                QObject *parent)
{
  setSmallestMz(other.m_smallestMz);
  setGreatestMz(other.m_greatestMz);
  setBinningType(other.m_binningType);
  setBinSizeModel(other.m_binSizeModel);
  setBinSizeDivisor(other.m_binSizeDivisor);
  setDecimalPlaces(other.m_decimalPlaces);
  setRemoveZeroValDataPoints(other.m_removeZeroValDataPoints);

  setParent(parent);
}

// For use in JavaScript.
void
MzIntegrationParams::initialize(const MzIntegrationParams *other_p,
                                QObject *parent)
{
  Q_ASSERT(other_p != nullptr);
  initialize(*other_p, parent);
}

// Initialize this MzIntegrationParams using other but only for members that
// were effectively set upon reading from a text string (see initialize
// (QString)).
void
MzIntegrationParams::initialize(MzIntegrationParams &other,
                                InitializationResult initialization_results)
{
  if(static_cast<bool>(
       initialization_results &
       pappso::MzIntegrationParams::InitializationResult::BINNING_TYPE))
    setBinningType(other.m_binningType);

  if(static_cast<bool>(
       initialization_results &
       pappso::MzIntegrationParams::InitializationResult::BIN_SIZE_MODEL))
    setBinSizeModel(other.m_binSizeModel);

  if(static_cast<bool>(
       initialization_results &
       pappso::MzIntegrationParams::InitializationResult::BIN_SIZE_DIVISOR))
    setBinSizeDivisor(other.m_binSizeDivisor);

  if(static_cast<bool>(
       initialization_results &
       pappso::MzIntegrationParams::InitializationResult::DECIMAL_PLACES))
    setDecimalPlaces(other.m_decimalPlaces);
}

void
MzIntegrationParams::setSmallestMz(double value)
{
  if(m_smallestMz != value)
    m_smallestMz = value;

  emit smallestMzChanged();
}

void
MzIntegrationParams::updateSmallestMz(double value)
{
  if(value == m_smallestMz)
    return;

  m_smallestMz = value;
  emit smallestMzChanged();
}

double
MzIntegrationParams::getSmallestMz() const
{
  return m_smallestMz;
}

void
MzIntegrationParams::setGreatestMz(double value)
{
  if(m_greatestMz == value)
    return;

  m_greatestMz = value;
  emit greatestMzChanged();
}

void
MzIntegrationParams::updateGreatestMz(double value)
{
  if(value > m_greatestMz)
    {
      m_greatestMz = value;
      emit greatestMzChanged();
    }
}

double
MzIntegrationParams::getGreatestMz() const
{
  return m_greatestMz;
}

void
MzIntegrationParams::setMzValues(double smallest, double greatest)
{
  setSmallestMz(smallest);
  setGreatestMz(greatest);
}

void
MzIntegrationParams::setBinningType(
  MzIntegrationParams::BinningType binning_type)
{
  if(m_binningType == binning_type)
    return;

  m_binningType = binning_type;
  emit binningTypeChanged();
}

MzIntegrationParams::BinningType
MzIntegrationParams::getBinningType() const
{
  return m_binningType;
}


int
MzIntegrationParams::getDecimalPlaces() const
{
  return m_decimalPlaces;
}

void
MzIntegrationParams::setBinSizeModel(pappso::PrecisionPtr bin_size_model_p)
{
  if(m_binSizeModel == bin_size_model_p)
    return;

  m_binSizeModel = bin_size_model_p;

  if(m_binSizeModel == nullptr)
    m_binSizeModel = pappso::PrecisionFactory::getResInstance(40000);

  emit binSizeModelChanged();
}

pappso::PrecisionPtr
MzIntegrationParams::getBinSizeModel() const
{
  return m_binSizeModel;
}

void
MzIntegrationParams::setBinSizeDivisor(int divisor)
{
  if(m_binSizeDivisor == divisor)
    return;

  m_binSizeDivisor = divisor;
  emit binSizeDivisorChanged();
}

int
MzIntegrationParams::getBinSizeDivisor() const
{
  return m_binSizeDivisor;
}

void
MzIntegrationParams::setIndicativeBinSize(double value)
{
  m_indicativeBinSize = value;
  emit indicativeBinSizeChanged();
}

double
MzIntegrationParams::getIndicativeBinSize() const
{
  return m_indicativeBinSize;
}

void
MzIntegrationParams::setDecimalPlaces(int decimal_places)
{
  if(m_decimalPlaces == decimal_places)
    return;

  m_decimalPlaces = decimal_places;
  emit decimalPlacesChanged();
}

void
MzIntegrationParams::setRemoveZeroValDataPoints(bool removeOrNot)
{
  if(m_removeZeroValDataPoints == removeOrNot)
    return;

  m_removeZeroValDataPoints = removeOrNot;
  emit removeZeroValDataPointsChanged();
}

bool
MzIntegrationParams::isRemoveZeroValDataPoints() const
{
  return m_removeZeroValDataPoints;
}

//! Reset the instance to default values.
void
MzIntegrationParams::reset()
{
  // Each function handles the emission of the corresponding changed
  // notification.
  setSmallestMz(std::numeric_limits<double>::max());
  setGreatestMz(std::numeric_limits<double>::min());
  setBinningType(BinningType::ARBITRARY);
  setBinSizeModel(pappso::PrecisionFactory::getResInstance(40000));
  setBinSizeDivisor(1);
  setDecimalPlaces(-1);
  setRemoveZeroValDataPoints(true);
}

bool
MzIntegrationParams::isValid() const
{
  int errors = 0;

  if(m_binningType != MzIntegrationParams::BinningType::NONE)
    {
      // qDebug() << "m_smallestMz:" << m_smallestMz;
      // qDebug() << "smallest is max:" << (m_smallestMz ==
      // std::numeric_limits<double>::max());
      errors += (m_smallestMz == std::numeric_limits<double>::max() ? 1 : 0);

      // qDebug() << "m_greatestMz:" << m_greatestMz;
      // qDebug() << "greatest is min:" << (m_greatestMz ==
      // std::numeric_limits<double>::min());
      errors += (m_greatestMz == std::numeric_limits<double>::min() ? 1 : 0);
    }

  if(errors)
    {
      qCritical() << "The m/z integration parameters are invalid.";
    }

  return !errors;
}

bool
MzIntegrationParams::hasValidMzRange() const
{
  return (m_smallestMz < std::numeric_limits<double>::max()) &&
         (m_greatestMz >= std::numeric_limits<double>::min());
}

std::vector<double>
MzIntegrationParams::createBins()
{
  // qDebug() << "mp_precision:" << mp_precision->toString();

  std::vector<double> bins;

  if(m_binningType == MzIntegrationParams::BinningType::NONE)
    {
      // If no binning is to be performed, fine.
      return bins;
    }
  else if(m_binningType == MzIntegrationParams::BinningType::ARBITRARY)
    {
      // Use only data in the MzIntegrationParams member data.
      return createArbitraryBins();
    }
  else if(m_binningType == MzIntegrationParams::BinningType::DATA_BASED)
    {
      qFatal() << "Programming error. "
                  "Please use the createBins(pappso::MassSpectrumCstSPtr "
                  "mass_spectrum_csp) overload.";
    }

  return bins;
}

std::vector<double>
MzIntegrationParams::createBins(pappso::MassSpectrumCstSPtr mass_spectrum_csp)
{
  // qDebug();

  std::vector<double> bins;

  if(m_binningType == MzIntegrationParams::BinningType::NONE)
    {
      // If no binning is to be performed, fine.
      return bins;
    }
  else if(m_binningType == MzIntegrationParams::BinningType::ARBITRARY)
    {
      // Use only data in the MzIntegrationParams member data.
      return createArbitraryBins();
    }
  else if(m_binningType == MzIntegrationParams::BinningType::DATA_BASED)
    {
      // qDebug();

      // Use the first spectrum to perform the data-based bins

      return createDataBasedBins(mass_spectrum_csp);
    }

  return bins;
}

std::vector<double>
MzIntegrationParams::createArbitraryBins()
{
  // Now starts the tricky stuff. Depending on the binning size model, we need
  // to take diverse actions.

  if(!isValid())
    qFatal() << "Programming error. The MzIntegrationParams::BinningLogic is "
                "not valid, cannot create bins.";

  // qDebug() << "Binning logic:" << toString();

  qDebug() << qSetRealNumberPrecision(6) << "The smallest and greatest m/z values:" << m_smallestMz
           << "-" << m_greatestMz;

  // Compute the number of decimals required to faithfully account for the
  // precision needed to represent the binned data. We do this by computing
  // some sort of bin size and checking how many decimals we need to described it.

  // We cannot have a smallest m/z that is 0 because that impairs the computation of the
  // bins (see below). In case the m_smallestMz value is 0 we craft one temporary value
  // by computing half of the range:

  // In certain circumstances, the bin size is not enough to properly render
  // hyper-high resolution data (like the theoretical isotopic cluster data
  // generated in silico). In that case, the bin size, computed using the
  // precision object, is divided by the m_binSizeDivisor, which normally is
  // set to 6 as the default because that is the empirical observation that
  // it gives the most nicely shaped peaks.
  Q_ASSERT(m_binSizeDivisor >= 1);


  // Instantiate the vector of mz double_s that we'll feed with the bins.
  std::vector<double> bins;

  double bin_size;

  if(!m_smallestMz)
    {
      if(m_indicativeBinSize)
        {
          // The caller has intelligently set an indicative bin size. Use it.
          bin_size = m_indicativeBinSize;
          qDebug() << qSetRealNumberPrecision(6)
                   << "The indicative bin size is being used:" << bin_size;
        }
      else
        {
          double half_way_mz = 0;
          half_way_mz        = (m_greatestMz - m_smallestMz) / 2;

          qDebug() << " Target min_mz:" << m_smallestMz << "and max_mz:" << m_greatestMz
                   << "half_way_mz:" << half_way_mz;
          bin_size = m_binSizeModel->delta(half_way_mz) / m_binSizeDivisor;
          qDebug() << qSetRealNumberPrecision(6)
                   << "The m/z range half way strategy is being used for bin size:" << bin_size;
        }
    }
  else
    {
      bin_size = m_binSizeModel->delta(m_smallestMz) / m_binSizeDivisor;
      qDebug() << qSetRealNumberPrecision(6) << "The normally computed bin size:" << bin_size;
    }

  qDebug() << qSetRealNumberPrecision(6) << "The bin size was computed to be" << bin_size;

  // Only compute the decimal places if they were not configured already.
  if(m_decimalPlaces == -1)
    {
      // qDebug() << "Now checking how many decimal places are needed.";

      // We want as many decimal places as there are 0s between the integral
      // part of the double and the first non-0 cipher.  For example, if
      // binSize is 0.004, zero decimals is 2 and m_decimalPlaces is set to 3,
      // because we want decimals up to 4 included.

      m_decimalPlaces = pappso::Utils::zeroDecimalsInValue(bin_size) + 1;

      qDebug() << "With binSize" << bin_size
               << " m_decimalPlaces was computed to be:" << m_decimalPlaces;
    }

  qDebug() << "m_decimalPlaces: " << m_decimalPlaces;

  // Now that we have defined the value of m_decimalPlaces, let's use that
  // value.
  double first_mz =
    Utils::roundValueToDecimalPlaces(m_smallestMz, m_decimalPlaces, /*round_up*/ false);
  double last_mz =
    Utils::roundValueToDecimalPlaces(m_greatestMz, m_decimalPlaces, /*round_up*/ true);

  qDebug() << qSetRealNumberPrecision(10)
           << "After having accounted for the decimals, new min/max values:"
           << "Very first data point to start from:" << first_mz
           << "Very last data point to reach: " << last_mz;

  double previous_mz_bin;
  double current_mz;

  // Store that very first value for later use in the loop.
  // The bins are nothing more than:
  //
  // 1. The first mz (that is the smallest mz value)
  // 2. A sequence of mz values corresponding to that first mz value
  // incremented by the bin size calculated each time using the precision
  // facility (res, ppm, dalton logic).

  bins.push_back(first_mz);

  // The previous_mz_bin cannot be 0, otherwise the m_binSizeModel->delta() will
  // return 0 and we won't be able to create bins.

  if(!first_mz)
    {
      bins.push_back(bin_size);
      previous_mz_bin = bin_size;
    }
  else
    {
      previous_mz_bin = first_mz;
    }

  // qDebug() << qSetRealNumberPrecision(6) << "Pushed back first rounded mz:" << previous_mz_bin;

  // Now continue adding mz values until we have reached the end of the
  // spectrum, that is the max_mz value, as converted using the decimals to
  // last_mz.

  // debugCount value used below for debugging purposes.
  // int debugCount = 0;

  while(previous_mz_bin <= last_mz)
    {
      // The size of bin is calculated dynamically using the
      // precision object that computes it with its specifications:
      // res or ppm or dalton.
      current_mz = previous_mz_bin + m_binSizeModel->delta(previous_mz_bin) / m_binSizeDivisor;

      // qDebug() << qSetRealNumberPrecision(6) << "previous mz bin: " << previous_mz_bin
      //          << "and current mz: " << current_mz;

      // Now apply on the obtained mz value the decimals that were either set
      // or computed earlier.

      double current_rounded_mz = Utils::roundValueToDecimalPlaces(current_mz,
                                                                   m_decimalPlaces,
                                                                   /*round_up*/ true);

      // qDebug() << qSetRealNumberPrecision(50)
      //          << "after rounding, current mz becomes: " << current_rounded_mz;

      // If rounding makes the new value identical to the previous one, then
      // that means that we need to decrease roughness.

      if(current_rounded_mz == previous_mz_bin)
        {
          ++m_decimalPlaces;

          qDebug() << "It was required to increment decimal places to" << m_decimalPlaces;
          current_rounded_mz = Utils::roundValueToDecimalPlaces(current_mz,
                                                                m_decimalPlaces,
                                                                /*round_up*/ true);

          qDebug().noquote() << "Because current rounded mz is equal to previous mz bin, we had to "
                                "increment decimal places by one while creating the bins "
                                "in MzIntegrationParams::BinningType::ARBITRARY mode.";
        }

      bins.push_back(current_rounded_mz);

      // qDebug() << qSetRealNumberPrecision(50)
      //          << "Pushed back current rounded mz:" << current_rounded_mz;


      // Use the local_mz value for the storage of the previous mz bin.
      previous_mz_bin = current_rounded_mz;
    }

  // #if 0

  QString bins_with_delta = binsToStringWithDeltas(bins);

  QString file_name = "/tmp/massSpecArbitraryBins.txt-at-" +
                      QDateTime::currentDateTime().toString("yyyyMMdd-HH-mm-ss");

  qDebug() << "Writing the list of bins setup in the mass spectrum in file " << file_name;

  Q_ASSERT(Utils::writeToFile(bins_with_delta, file_name));

  // #endif

  qDebug() << "Prepared " << bins.size() << "arbitrary bins starting with mz" << bins.front()
           << "ending with mz" << bins.back();

  return bins;
}

std::vector<double>
MzIntegrationParams::createDataBasedBins(
  pappso::MassSpectrumCstSPtr mass_spectrum_csp)
{
  // The mass spectrum passed as parameters has intrinsic bins in it. These bins
  // are nothing else than the succession of m/z values in it.

  // The very first thing to do is replicate that spectrum into a local copy
  // of bins (simple double values), and during that step recompute the m/z
  // value (x value) according to the decimals settings in this instance, if
  // that is required (that is m_decimalPlaces != -1).

  QList<double> bins;
  QList<double> deltas;
  QList<double> resolutions;

  double left_mz_value  = 0;
  double right_mz_value = 0;
  double mz_delta       = 0;
  double resolution     = 0;

  // We need three bins to compute a number of values.
  if(mass_spectrum_csp->size() < 3)
    {
      std::vector<double> bins_vector(bins.constBegin(), bins.constEnd());
      return bins_vector;
    }

  // Make sure the spectrum is sorted, as this function takes for granted
  // that the DataPoint instances are sorted in ascending x (== mz) value
  // order.
  pappso::MassSpectrum mass_spectrum_copy = *mass_spectrum_csp;
  mass_spectrum_copy.sortMz();

  std::vector<pappso::DataPoint>::const_iterator iterator_const =
    mass_spectrum_copy.cbegin();

  // We need to see the first value
  left_mz_value = iterator_const->x;

  if(m_decimalPlaces != -1)
    left_mz_value =
      Utils::roundValueToDecimalPlaces(left_mz_value, m_decimalPlaces, /*round_up*/ true);

  qDebug() << qSetRealNumberPrecision(6)
           << "left_mz_value in the template mass spectrum:" << left_mz_value;

  bins.append(left_mz_value);
  // No delta nor resolution can be computed for the first m/z
  // because their computation requires to compare the second m/z with
  // the first m/z, and in turn for all the remaining m/z values.
  deltas.append(0.0);
  resolutions.append(0.0);

  // We used the first bin, go to the next!
  ++iterator_const;
  while(iterator_const != mass_spectrum_copy.cend())
    {
      right_mz_value = iterator_const->x;

      qDebug() << qSetRealNumberPrecision(6)
               << "right_mz_value:" << right_mz_value;

      if(m_decimalPlaces != -1)
        right_mz_value =
          Utils::roundValueToDecimalPlaces(right_mz_value, m_decimalPlaces, /*round_up*/ true);

      bins.append(right_mz_value);

      mz_delta = right_mz_value - left_mz_value;
      deltas.append(mz_delta);
      Q_ASSERT(mz_delta != 0.0);

      resolution = right_mz_value / mz_delta;
      resolutions.append(resolution);

      left_mz_value = right_mz_value;
      ++iterator_const;
    }

  // At this point we have the bins that match those in the template mass
  // spectrum.

  // #if 0

  // We want to check them.
  std::vector<double> bins_vector(bins.constBegin(), bins.constEnd());

  QString bins_with_delta = binsToStringWithDeltas(bins_vector);

  QString file_name =
    "/tmp/massSpecDataBasedTemplateBinsWithDeltas.txt-at-" +
    QDateTime::currentDateTime().toString("yyyyMMdd-HH-mm-ss");

  // qDebug() << "Writing the list of bins setup in the mass spectrum in file "
  // << file_name;

  Q_ASSERT(Utils::writeToFile(bins_with_delta, file_name));

  // #endif

  // Done, we now have bins and deltas in between each bin and its following
  // bin.

  // Now, there are specificities:
  //
  // If the user has set m_smallestMz and/or m_greatestMz, then we
  // still have to work to do. That is because the user wants us to keep
  // the binning as found in the template mass spectrum passed as parameter, but
  // they also want that the m/z range of the bins be potentially larger, if
  // either m_smallestMz is smaller than the first bin value or if
  // m_greatestMz is larger than the last bin value, or BOTH.

  // The difficulty here is that we need to know the logic that has been used
  // in the first place for the generation of the bins in the template mass
  // spectrum.

  // Some metrics:

  // We could check if the bins have the same width all along the m/z range.

  // Delta should be constant if the bins were calculated with dalton logic.

  bool uniform_deltas      = false;
  std::size_t deltas_count = deltas.size();
  // We cannot really take the first delta because it is 0.00000.
  double first_delta       = deltas.at(1);
  double middle_delta      = deltas.at(deltas_count / 2);
  double last_delta        = deltas.at(deltas_count - 1);

  if(first_delta == middle_delta && middle_delta == last_delta)
    uniform_deltas = true;

  // qDebug() << "Deltas are uniform or not?" << uniform_deltas;
  // qDebug() << qSetRealNumberPrecision(6) << "First delta:" << first_delta
  //          << "Middle delta:" << middle_delta << "Last delta:" << last_delta;

  // Resolution should be constant if the bins were calculated with resolution
  // logic. Remember : Res = m/z / Delta m/z.
  bool uniform_resolutions      = false;
  std::size_t resolutions_count = resolutions.size();
  // We cannot really take the first resolution because it is 0.00000.
  double first_resolution       = resolutions.at(01);
  double middle_resolution      = resolutions.at(resolutions_count / 2);
  double last_resolution        = resolutions.at(resolutions_count - 1);

  if(first_resolution == middle_resolution &&
     middle_resolution == last_resolution)
    uniform_resolutions = true;

  // qDebug() << "Deltas are uniform or not?" << uniform_resolutions;
  // qDebug() << qSetRealNumberPrecision(6) << "First resolution:" <<
  // first_resolution
  //          << "Middle resolution:" << middle_resolution << "Last resolution:"
  //          << last_resolution;

  // Now that we have the metric about the bins, we can start checking the other
  // parameters of the creation of bins:
  // m_smallestMz vs m_greatestMz

  // In theory, m_smallestMz and m_greatestMz were designed for the arbitrary
  // binning method, but it is imaginable that one would want to combine to an
  // existing spectrum a new spectrum that is larger than the existing
  // one, either on the left or on the right.

  double first_bin_mz = bins.first();
  double last_bin_mz  = bins.last();

  // qDebug() << qSetRealNumberPrecision(6) << "smallest_mz:" << m_smallestMz
  //          << "greatest_mz:" << m_greatestMz;

  // Now we can check if the [m_smallestMz -- m_greatestMz] m/z value range
  // extends farther on the left or on the right or both compared to the bins
  // range.

  // Now that we have the real smallest and greatest m/z values that might be
  // std::max() and std::min() respectively if not set by the user. So all we
  // have to do is check them against the first and last bin values.

  // The list of prepended bins on the left of the existing bins. By starting
  // with first_bin_mz, we maintain the phase with the bins in the mass spectrum
  // passed as parameter.

  if(m_smallestMz < first_bin_mz)
    {
      // qDebug() << "A starting m/z value is smaller than the first bin in the
      // "
      //             "spectrum";

      // We will have to craft bins to the left of first_bin_mz up to
      // smallest_mz. There are going to be different strategies depending on
      // what we discovered above. We will prepend the newly created bins to the
      // bins QList (thanks QList).

      double bin_mz = first_bin_mz;

      if(uniform_deltas)
        {
          // That is the simplest situation.

          while(bin_mz > m_smallestMz)
            {
              double new_bin_mz = bin_mz - first_delta;
              bins.prepend(new_bin_mz);
              bin_mz = new_bin_mz;
            }
        }
      else if(uniform_resolutions)
        {
          // Remember: Res = m/z / Delta m/z.

          while(bin_mz > m_smallestMz)
            {
              double new_bin_mz = bin_mz - (bin_mz / first_resolution);
              bins.prepend(new_bin_mz);
              bin_mz = new_bin_mz;
            }
        }
    }

  // Now make sure we append any necessary bin (same logic as above).
  // By starting with last_bin_mz, we maintain the phase with the bins in the
  // mass spectrum passed as parameter.

  if(m_greatestMz > last_bin_mz)
    {
      // qDebug() << "A stopping m/z value is greater than the last bin in the "
      //             "spectrum";

      // Craft bins to the right of the bins m/z range, that is append
      // new bins.

      double bin_mz = last_bin_mz;

      if(uniform_deltas)
        {
          // That is the simplest situation.

          while(bin_mz < m_greatestMz)
            {
              double new_bin_mz = bin_mz + first_delta;
              bins.append(new_bin_mz);
              bin_mz = new_bin_mz;
            }
        }
      else if(uniform_resolutions)
        {
          // Remember: Res = m/z / Delta m/z.

          while(bin_mz > m_greatestMz)
            {
              double new_bin_mz = bin_mz + (bin_mz / first_resolution);
              bins.append(new_bin_mz);
              bin_mz = new_bin_mz;
            }
        }
    }

  // At this point we should have bins over the full range of m/z values
  // required by the user and in phase with those transmitted via the
  // mass spectrum argument.

  // Convert to std::vector<double> and return.
  std::vector<double> full_bins_vector(bins.constBegin(), bins.constEnd());

#if 0

  bins_with_delta = binsToStringWithDeltas(full_bins_vector);

  file_name = "/tmp/massSpecDataBasedFullBinsWithDeltas.txt-at-" +
              QDateTime::currentDateTime().toString("yyyyMMdd-HH-mm-ss");

  qDebug() << "Writing the list of bins setup in the mass spectrum in file "
           << file_name;

  Q_ASSERT(Utils::writeToFile(bins_with_delta, file_name));

#endif

  // qDebug() << "Prepared " << bins.size() << "data-based bins starting with
  // mz"
  //          << bins.front() << "ending with mz" << bins.back();

  return full_bins_vector;
}

std::vector<double>
MzIntegrationParams::createDataBasedBinsOld(
  pappso::MassSpectrumCstSPtr mass_spectrum_csp)
{
  // The bins must be calculated using those in mass_spectrum_csp as a template.

  // qDebug();

  std::vector<double> bins;

  if(mass_spectrum_csp->size() < 2)
    return bins;

  // Make sure the spectrum is sorted, as this function takes for granted
  // that the DataPoint instances are sorted in ascending x (== mz) value
  // order.
  pappso::MassSpectrum sorted_mass_spectrum = *mass_spectrum_csp;
  sorted_mass_spectrum.sortMz();

  double min_mz = m_smallestMz;

  // qDebug() << "The min_mz:" << min_mz;

  if(m_decimalPlaces != -1)
    min_mz = ceil((min_mz * pow(10, m_decimalPlaces)) - 0.49) /
             pow(10, m_decimalPlaces);

  // Two values for the definition of a MassSpectrumBin.

  // The first value of the mz range that defines the bin. This value is part
  // of the bin.
  double start_mz_in = min_mz;

  // The second value of the mz range that defines the bin. This value is
  // *not* part of the bin.
  double end_mz_out;

  std::vector<pappso::DataPoint>::const_iterator it =
    sorted_mass_spectrum.begin();

  double prev_mz = it->x;

  if(m_decimalPlaces != -1)
    prev_mz = ceil((prev_mz * pow(10, m_decimalPlaces)) - 0.49) /
              pow(10, m_decimalPlaces);

  ++it;

  while(it != sorted_mass_spectrum.end())
    {
      double next_mz = it->x;

      if(m_decimalPlaces != -1)
        next_mz = ceil((next_mz * pow(10, m_decimalPlaces)) - 0.49) /
                  pow(10, m_decimalPlaces);

      double step = next_mz - prev_mz;
      end_mz_out  = start_mz_in + step;

      if(m_decimalPlaces != -1)
        end_mz_out = ceil((end_mz_out * pow(10, m_decimalPlaces)) - 0.49) /
                     pow(10, m_decimalPlaces);

      // The data point that is crafted has a 0 y-value. The binning must
      // indeed not create artificial intensity data.

      // qDebug() << "Pushing back bin:" << start_mz_in << end_mz_out;

      bins.push_back(start_mz_in);

      // Prepare next bin
      start_mz_in = end_mz_out;

      // Update prev_mz to be the current one for next iteration.
      prev_mz = next_mz;

      // Now go to the next DataPoint instance.
      ++it;
    }

#if 0

  QString fileName = "/tmp/massSpecDataBasedBins.txt";

  qDebug() << "Writing the list of bins setup in the "
  "mass spectrum in file "
  << fileName;

  QFile file(fileName);
  file.open(QIODevice::WriteOnly);

  QTextStream fileStream(&file);

  for(auto &&bin : m_bins)
    fileStream << QString("[%1-%2]\n")
    .arg(bin.startMzIn, 0, 'f', 10)
    .arg(bin.endMzOut, 0, 'f', 10);

  fileStream.flush();
  file.close();

  qDebug() << "elements."
  << "starting with mz" << m_bins.front().startMzIn << "ending with mz"
  << m_bins.back().endMzOut;

#endif

  return bins;
}

// This is for documentation, not for outputting the string used in the
// settings that are saved on disk.
QString
MzIntegrationParams::toString(int offset, const QString &spacer) const
{
  // The space-containing string that reflects the offset at which
  // new text lines should be added to start with.
  QString offset_lead;

  for(int iter = 0; iter < offset; ++iter)
    offset_lead += spacer;

  QString text  = offset_lead;
  text         += "m/z integration parameters:\n";

  QString new_lead = QString("%1%2").arg(offset_lead, spacer);

  text += new_lead;
  if(m_smallestMz != std::numeric_limits<double>::max())
    text.append(
      QString::asprintf("Smallest (first) m/z: %.6f\n", m_smallestMz));

  text += new_lead;
  if(m_greatestMz != std::numeric_limits<double>::min())
    text.append(QString::asprintf("Greatest (last) m/z: %.6f\n", m_greatestMz));

  text += new_lead;
  text += QString("Remove 0-val data points: %1\n")
            .arg(m_removeZeroValDataPoints ? "true" : "false");

  text += new_lead;
  text.append("Binning logic:\n");

  new_lead += spacer;

  text += new_lead;
  text.append(
    QString("Binning type:%1\n").arg(::pappso::binningTypeMap[m_binningType]));

  text += new_lead;
  text.append(QString("Bin size model: %1\n").arg(m_binSizeModel->toString()));

  text += new_lead;
  text.append(QString("Bin size divisor: %2\n").arg(m_binSizeDivisor));

  text += new_lead;
  text.append(QString("Decimal places: %1\n").arg(m_decimalPlaces));

  return text;
}

// This version is used to craft the string that enables the initialization.
QString
MzIntegrationParams::toString() const
{
  QString text;

  // In the string for saving settings, we do not ouput the m/z values.
  text.append(
    QString("Binning type:%1\n").arg(::pappso::binningTypeMap[m_binningType]));
  text.append(QString("Bin size model: %1\n").arg(m_binSizeModel->toString()));
  text.append(QString("Bin size divisor: %2\n").arg(m_binSizeDivisor));
  text.append(QString("Decimal places:%1\n").arg(m_decimalPlaces));
  text.append(
    QString("Remove 0-val data points:%1\n").arg(m_removeZeroValDataPoints));

  // qDebug().noquote() << "Returning text:\n" << text;

  return text;
}

QString
MzIntegrationParams::binsToStringWithDeltas(
  const std::vector<double> bins) const
{
  QString bins_with_delta;
  double previous_bin_value = 0;

  for(auto &&value : bins)
    {
      double delta     = value - previous_bin_value;
      bins_with_delta += QString("%1 - %2\n")
                           .arg(value, 0, 'f', m_decimalPlaces)
                           .arg(delta, 0, 'f', m_decimalPlaces);
      previous_bin_value = value;
    }

  return bins_with_delta;
}

void
MzIntegrationParams::registerJsConstructor(QJSEngine *engine)
{
  // qDebug() << "registerJsConstructor for MzIntegrationParams to QJSEngine.";

  if(engine == nullptr)
    {
      qFatal() << "Cannot register class: engine is null";
    }

  QJSValue pappso_root_property;
  QJSValue pappso_enums_property;

  if(engine->globalObject().hasProperty("pappso"))
    {
      qDebug() << "Global object property 'pappso' already exists.";
      pappso_root_property = engine->globalObject().property("pappso");

      if(pappso_root_property.hasProperty("Enums"))
        {
          pappso_enums_property = pappso_root_property.property("Enums");
        }
      else
        {
          pappso_enums_property = engine->newObject();
          pappso_root_property.setProperty("Enums", pappso_enums_property);
        }
    }
  else
    {
      qDebug() << "Global object property 'pappso' not found.";
      pappso_root_property  = engine->newObject();
      pappso_enums_property = engine->newObject();
      pappso_root_property.setProperty("Enums", pappso_enums_property);
      engine->globalObject().setProperty("pappso", pappso_root_property);
    }

  QJSValue enumObject;

  // First the BinningType enum
  enumObject = engine->newObject();

  enumObject.setProperty(
    "NONE", static_cast<int>(pappso::MzIntegrationParams::BinningType::NONE));
  enumObject.setProperty(
    "DATA_BASED",
    static_cast<int>(pappso::MzIntegrationParams::BinningType::DATA_BASED));
  enumObject.setProperty(
    "ARBITRARY",
    static_cast<int>(pappso::MzIntegrationParams::BinningType::ARBITRARY));

  pappso_enums_property.setProperty("BinningType", enumObject);

  // Second the InitializationResult enum
  enumObject = engine->newObject();
  enumObject.setProperty(
    "DEFAULT",
    static_cast<int>(
      pappso::MzIntegrationParams::InitializationResult::DEFAULT));
  enumObject.setProperty(
    "fromSettingsBinSizeModelPartial",
    static_cast<int>(pappso::MzIntegrationParams::InitializationResult::
                       BINNING_LOGIC_PARTIAL));
  enumObject.setProperty(
    "fromSettingsBinSizeModelFull",
    static_cast<int>(
      pappso::MzIntegrationParams::InitializationResult::BINNING_LOGIC_FULL));
  enumObject.setProperty(
    "fromSettingsFull",
    static_cast<int>(pappso::MzIntegrationParams::InitializationResult::FULL));

  pappso_enums_property.setProperty("InitializationResult", enumObject);

  // Finally gegister the class meta object as a constructor for the class
  QJSValue jsMetaObject =
    engine->newQMetaObject(&MzIntegrationParams::staticMetaObject);
  engine->globalObject().setProperty("MzIntegrationParams", jsMetaObject);
}

} // namespace pappso
