/* 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 <map>
#include <cmath>


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


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


/////////////////////// Local includes
#include "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<BinningType, QString> binningTypeMap{
  {BinningType::NONE, "NONE"},
  {BinningType::DATA_BASED, "DATA_BASED"},
  {BinningType::ARBITRARY, "ARBITRARY"}};


MzIntegrationParams::MzIntegrationParams()
{
  m_binningType = BinningType::NONE;
  mp_precision  = pappso::PrecisionFactory::getPpmInstance(20);
}


MzIntegrationParams::MzIntegrationParams(pappso::pappso_double minMz,
                                         pappso::pappso_double maxMz,
                                         BinningType binningType,
                                         int decimalPlaces,
                                         pappso::PrecisionPtr precisionPtr,
                                         int binSizeDivisor,
                                         bool removeZeroValDataPoints)
  : m_smallestMz(minMz),
    m_greatestMz(maxMz),
    m_binningType(binningType),
    m_decimalPlaces(decimalPlaces),
    mp_precision(precisionPtr),
    m_binSizeDivisor(binSizeDivisor),
    m_removeZeroValDataPoints(removeZeroValDataPoints)
{
  if(mp_precision == nullptr)
    mp_precision = pappso::PrecisionFactory::getPpmInstance(20);

  // qDebug() << "mp_precision:" << mp_precision->toString();
}


MzIntegrationParams::MzIntegrationParams(const MzIntegrationParams &other)
  : m_smallestMz(other.m_smallestMz),
    m_greatestMz(other.m_greatestMz),
    m_binningType(other.m_binningType),
    m_decimalPlaces(other.m_decimalPlaces),
    mp_precision(other.mp_precision),
    m_binSizeDivisor(other.m_binSizeDivisor),
    m_removeZeroValDataPoints(other.m_removeZeroValDataPoints)
{
  if(mp_precision == nullptr)
    mp_precision = pappso::PrecisionFactory::getResInstance(40000);

  // qDebug() << "mp_precision:" << mp_precision->toString();
}


MzIntegrationParams::~MzIntegrationParams()
{
}


MzIntegrationParams &
MzIntegrationParams::operator=(const MzIntegrationParams &other)
{
  if(this == &other)
    return *this;

  m_smallestMz  = other.m_smallestMz;
  m_greatestMz  = other.m_greatestMz;
  m_binningType = other.m_binningType;

  m_decimalPlaces = other.m_decimalPlaces;

  mp_precision = other.mp_precision;
  if(mp_precision == nullptr)
    mp_precision = pappso::PrecisionFactory::getPpmInstance(20);

  m_binSizeDivisor = other.m_binSizeDivisor;

  m_removeZeroValDataPoints = other.m_removeZeroValDataPoints;

  return *this;
}


void
MzIntegrationParams::setSmallestMz(pappso::pappso_double value)
{
  m_smallestMz = value;
}


void
MzIntegrationParams::updateSmallestMz(pappso::pappso_double value)
{
  m_smallestMz = m_smallestMz > value ? value : m_smallestMz;
}


pappso::pappso_double
MzIntegrationParams::getSmallestMz() const
{
  return m_smallestMz;
}


void
MzIntegrationParams::setGreatestMz(pappso::pappso_double value)
{
  m_greatestMz = value;
}


void
MzIntegrationParams::updateGreatestMz(pappso::pappso_double value)
{
  m_greatestMz = m_greatestMz < value ? value : m_greatestMz;
}


pappso::pappso_double
MzIntegrationParams::getGreatestMz() const
{
  return m_greatestMz;
}


void
MzIntegrationParams::setBinningType(BinningType binningType)
{
  m_binningType = binningType;
}


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


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


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

void
MzIntegrationParams::setPrecision(pappso::PrecisionPtr precisionPtr)
{
  mp_precision = precisionPtr;

  if(mp_precision == nullptr)
    mp_precision = pappso::PrecisionFactory::getDaltonInstance(0.05);
}

pappso::PrecisionPtr
MzIntegrationParams::getPrecision() const
{
  return mp_precision;
}


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


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


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


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


//! Reset the instance to default values.
void
MzIntegrationParams::reset()
{
  m_smallestMz  = std::numeric_limits<double>::min();
  m_greatestMz  = std::numeric_limits<double>::min();
  m_binningType = BinningType::NONE;

  // Special case for this member datum
  mp_precision = pappso::PrecisionFactory::getPpmInstance(20);

  m_binSizeDivisor          = 1;
  m_removeZeroValDataPoints = false;
}


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

  if(!m_binSizeDivisor)
    errors += 1;

  if(m_binningType != 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(mp_precision != nullptr)
      // qDebug() << mp_precision->toString();

      errors += (mp_precision == nullptr ? 1 : 0);
    }

  if(errors)
    {
      qDebug()
        << "The m/z integration parameters are not valid or do not apply...";
    }

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

      qFatal("Programming error.");
    }

  return bins;
}


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

  // qDebug();

  std::vector<double> bins;

  if(m_binningType == BinningType::NONE)
    {
      // If no binning is to be performed, fine.
      return bins;
    }
  else if(m_binningType == BinningType::ARBITRARY)
    {
      // Use only data in the MzIntegrationParams member data.
      return createArbitraryBins();
    }
  else if(m_binningType == 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 how the binning has been
  // configured, we need to take diverse actions.

  // qDebug() << "Bin size specification:" << mp_precision->toString();

  pappso::pappso_double min_mz = m_smallestMz;
  pappso::pappso_double max_mz = m_greatestMz;

  // qDebug() << "m_smallestMz:" << m_smallestMz
  //<< "m_greatestMz:" << m_greatestMz;

  // qDebug() << QString::asprintf("min_mz: %.6f\n", min_mz)
  //<< QString::asprintf("max_mz: %.6f\n", max_mz);

  pappso::pappso_double binSize;

  if(mp_precision->unit() == PrecisionUnit::res)
    {
      double resolution_based_bin_size = mp_precision->delta(min_mz);
      binSize = resolution_based_bin_size / m_binSizeDivisor;

      // qDebug() << "With res-based bin size, the uncorrected bin size:"
      //<< resolution_based_bin_size
      //<< "and the final binSize:" << binSize;
    }
  else
    binSize = mp_precision->delta(min_mz);

  // qDebug() << QString::asprintf(
  //"binSize is the precision delta for min_mz: %.6f\n", binSize);

  // 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(binSize) + 1;

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

  // Now that we have defined the value of m_decimalPlaces, let's use that
  // value.

  double first_mz = ceil((min_mz * std::pow(10, m_decimalPlaces)) - 0.49) /
                    pow(10, m_decimalPlaces);
  double last_mz =
    ceil((max_mz * pow(10, m_decimalPlaces)) - 0.49) / pow(10, m_decimalPlaces);

  // qDebug() << "After having accounted for the decimals, new min/max values:"
  //<< QString::asprintf("Very first data point: %.6f\n", first_mz)
  //<< QString::asprintf("Very last data point to reach: %.6f\n",
  // last_mz);

  // Instanciate the vector of mz double_s that we'll feed with the bins.

  std::vector<pappso::pappso_double> bins;

  // Store that very first value for later use in the loop.
  // The bins are notking more than:
  //
  // 1. The first mz (that is the smallest mz value found in all the spectra
  // 2. A sequence of mz values corresponding to that first mz value
  // incremented by the bin size.

  // Seed the root of the bin vector with the first mz value rounded above as
  // requested.
  pappso::pappso_double previous_mz_bin = first_mz;

  bins.push_back(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)
    {

      // qDebug() << "Now starting the bin creation loop.";

      // Calculate dynamically the precision delta according to the current mz
      // value.

      // double precision_delta = mp_precision->delta(previous_mz_bin);
      // qDebug() << "precision_delta: " << precision_delta;

      // 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 1 as the default, that is, it has no effect.

      double current_mz;

      if(mp_precision->unit() == PrecisionUnit::res)
        current_mz = previous_mz_bin +
                     (mp_precision->delta(previous_mz_bin) / m_binSizeDivisor);
      else
        current_mz = previous_mz_bin + mp_precision->delta(previous_mz_bin);

      // qDebug() << QString::asprintf(
      //"previous_mzBin: %.6f and current_mz: %.6f\n",
      // previous_mz_bin,
      // current_mz);

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

      double current_rounded_mz =
        ceil((current_mz * pow(10, m_decimalPlaces)) - 0.49) /
        pow(10, m_decimalPlaces);

      // qDebug() << QString::asprintf(
      //"current_mz: %.6f and current_rounded_mz: %.6f and previous_mzBin "
      //": % .6f\n ",
      // current_mz,
      // current_rounded_mz,
      // previous_mz_bin);

      // 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;

          current_rounded_mz =
            ceil((current_mz * pow(10, m_decimalPlaces)) - 0.49) /
            pow(10, m_decimalPlaces);

          // qDebug().noquote()
          //<< "Had to increment decimal places by one while creating the bins "
          //"in BinningType::ARBITRARY mode..";
        }

      bins.push_back(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 fileName = "/tmp/massSpecArbitraryBins.txt-at-" +
                     QDateTime::currentDateTime().toString("yyyyMMdd-HH-mm-ss");

  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 : bins)
    fileStream << QString("%1\n").arg(bin, 0, 'f', 10);

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

#endif

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

  return bins;
}


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

  // The bins in *this mass spectrum must be calculated starting from the
  // data in the mass_spectrum_csp parameter.

  // Instanciate the vector of mz double_s that we'll feed with the bins.

  std::vector<pappso::pappso_double> bins;

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

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

  pappso::pappso_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.
  pappso::pappso_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.
  pappso::pappso_double end_mz_out;

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

  pappso::pappso_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 != local_mass_spectrum.end())
    {
      pappso::pappso_double next_mz = it->x;

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

      pappso::pappso_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 got 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;
}


QString
MzIntegrationParams::toString(int offset, const QString &spacer) const
{
  QString lead;

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

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

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

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

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

  std::map<BinningType, QString>::iterator it;
  it = binningTypeMap.find(m_binningType);

  if(it == binningTypeMap.end())
    qFatal("Programming error.");

  text += lead;
  text += spacer;
  text.append(QString("Binning type: %1\n").arg(it->second.toLatin1().data()));

  // Only provide the details relative to the ARBITRARY binning type.

  if(m_binningType == BinningType::ARBITRARY)
    {
      text += lead;
      text += spacer;
      text += spacer;
      text.append(QString("Bin nominal size: %1\n")
                    .arg(mp_precision->getNominal(), 0, 'f', 6));

      text += lead;
      text += spacer;
      text += spacer;
      text.append(QString("Bin size: %2\n")
                    .arg(mp_precision->toString().toLatin1().data()));

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

  // Now other data that are independent of the bin settings.

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

  return text;
}

} // namespace pappso
