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

#include <cmath>
#include <limits>
#include <iomanip>
#include <iostream>
#include <type_traits>
#include <algorithm>


////////////////////////////// Qt includes
#include <QRegExp>
#include <QFileInfo>
#include <QDataStream>
#include <QDebug>
#include <QRegularExpressionMatch>


////////////////////////////// Local includes
#include <globals/globals.hpp>


namespace msXpS
{


//! Map relating the MassToleranceType to a textual representation
QMap<int, QString> massToleranceTypeMap{
  {MassToleranceType::MASS_TOLERANCE_NONE, "NONE"},
  {MassToleranceType::MASS_TOLERANCE_PPM, "PPM"},
  {MassToleranceType::MASS_TOLERANCE_MZ, "MZ"},
  {MassToleranceType::MASS_TOLERANCE_AMU, "AMU"},
  {MassToleranceType::MASS_TOLERANCE_RES, "RES"}};


//! Map relating the BinningType to a textual representation
QMap<int, QString> binningTypeMap{
  {BinningType::BINNING_TYPE_NONE, "NONE"},
  {BinningType::BINNING_TYPE_DATA_BASED, "DATA_BASED"},
  {BinningType::BINNING_TYPE_ARBITRARY, "ARBITRARY"}};


//! Define the character to use as a prefix delimiter in QSettings-based
// names.
/*!
 * \sa msXpS::ConfigSetting
 */
QChar gConfigKeyStringPrefixDelimiter = QChar('%');


//! Width of a Gaussian/Lorentzian curve.
/*!
 * Describe how many times the whole gaussian/lorentzian curve should span
 * around the centroid mz ratio. 4 means that the peak will span
 * : [ mz - (2 * FWHM) --> mz + (2 * FWHM) ]
 */
int FWHM_PEAK_SPAN_FACTOR = 4;


/**@{*/

//! Number of decimal places after the decimal symbol for atom masses.
/*!
 * \sa msXpSmassXpert::DecimalPlacesOptionsDlg
 */
int ATOM_DEC_PLACES = 10;

//! Number of decimal places after the decimal symbol for oligomer masses.
/*!
 * \sa msXpSmassXpert::DecimalPlacesOptionsDlg
 */
int OLIGOMER_DEC_PLACES = 5;

//! Number of decimal places after the decimal symbol for polymer masses.
/*!
 * \sa msXpSmassXpert::DecimalPlacesOptionsDlg
 */
int POLYMER_DEC_PLACES = 3;

//! Number of decimal places after the decimal symbol for pH/pKa values.
/*!
 * \sa msXpSmassXpert::DecimalPlacesOptionsDlg
 */
int PH_PKA_DEC_PLACES = 2;

/**@}*/

//! Regular expression that tracks the m/z,i pairs in text files.
// One val (intensity) value can be like this: 8.19179e-05
QRegularExpression gXyFormatMassDataRegExp = QRegularExpression(
  "^(\\d*\\.?\\d*[e]?[-]?[+]?\\d*)([^\\d^\\.^-]+)(-?\\d*\\.?\\d*[e]?[-]?[+]?"
  "\\d*)");


//! Regular expression that tracks the end of line in text files.
QRegularExpression gEndOfLineRegExp = QRegularExpression("^\\s+$");


//! Return the average of the double list. Also fill-in the variance and
//! standard deviation if the parameters are non-nullptr.
void
doubleListStatistics(QList<double> list,
                     double *sum,
                     double *average,
                     double *variance,
                     double *stdDeviation,
                     double *nonZeroSmallest,
                     double *smallest,
                     double *smallestMedian,
                     double *greatest)
{
  if(sum == Q_NULLPTR || average == Q_NULLPTR || variance == Q_NULLPTR ||
     stdDeviation == Q_NULLPTR || nonZeroSmallest == Q_NULLPTR ||
     smallest == Q_NULLPTR || greatest == Q_NULLPTR ||
     smallestMedian == Q_NULLPTR)
    qFatal(
      "Fatal error at %s@%d -- %s(). "
      "Pointers cannot be nullptr."
      "Program aborted.",
      __FILE__,
      __LINE__,
      __FUNCTION__);

  // Sort the list, we'll need it sorted to compute the median value.
  qSort(list.begin(), list.end());

  int count = list.size();

  if(count == 0)
    return;

  // Sorted list, the smallest is the first.
  *smallest = list.first();
  // The greatest is the last.
  *greatest = list.last();

  *sum             = 0;
  *nonZeroSmallest = std::numeric_limits<double>::max();

  for(int iter = 0; iter < count; ++iter)
    {
      double current = list.at(iter);
      *sum += current;

      // If current is non-zero, then take its value into account.
      if(current > 0 && current < *nonZeroSmallest)
        *nonZeroSmallest = current;
    }

  *average = *sum / count;

  double varN = 0;

  for(int iter = 0; iter < count; ++iter)
    {
      varN += (list.at(iter) - *average) * (list.at(iter) - *average);
    }

  *variance     = varN / count;
  *stdDeviation = sqrt(*variance);

  // Now the median value

  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
  //<< "count:" << count;

  if(count == 1)
    {
      *smallestMedian = list.first();
    }
  else
    {
      if(count % 2 == 0)
        *smallestMedian = (list.at(count / 2 - 1) + list.at(count / 2)) / 2;
      else
        *smallestMedian = list.at(count / 2 + 1);
    }
}


//! Tell if both double values, are equal within the double representation
//! capabilities of the platform.
bool
almostEqual(double value1, double value2, int decimalPlaces)
{
  // QString value1String = QString("%1").arg(value1,
  // 0, 'f', 60);
  // QString value2String = QString("%1").arg(value2,
  // 0, 'f', 60);

  // qWarning() << __FILE__ << __LINE__ << __FUNCTION__
  //<< "value1:" << value1String << "value2:" << value2String;

  // The machine epsilon has to be scaled to the magnitude of the values used
  // and multiplied by the desired precision in ULPs (units in the last place)
  // (decimal places).

  double valueSum = std::abs(value1 + value2);
  // QString valueSumString = QString("%1").arg(valueSum,
  // 0, 'f', 60);

  double valueDiff = std::abs(value1 - value2);
  // QString valueDiffString = QString("%1").arg(valueDiff,
  // 0, 'f', 60);

  double epsilon = std::numeric_limits<double>::epsilon();
  // QString epsilonString = QString("%1").arg(epsilon,
  // 0, 'f', 60);

  double scaleFactor = epsilon * valueSum * decimalPlaces;
  // QString scaleFactorString = QString("%1").arg(scaleFactor,
  // 0, 'f', 60);

  // qWarning() << "valueDiff:" << valueDiffString << "valueSum:" <<
  // valueSumString << "epsilon:" << epsilonString << "scaleFactor:" <<
  //scaleFactorString;

  bool res = valueDiff < scaleFactor
             // unless the result is subnormal:
             || valueDiff < std::numeric_limits<double>::min();

  // qWarning() << __FILE__ << __LINE__ << __FUNCTION__
  //<< "returning res:" << res;

  return res;
}


//! Provides a text stream handle to the standard output.
/*!
 * Use:   msXpS::qStdOut() << __FILE__ << __LINE__
 *                         << "text to the standard output,"
 *                         << "not the error standard output."
 *
 * \return a reference to a static QTextStream that directs to the standard
 * output.
 */
QTextStream &
qStdOut()
{
  static QTextStream ts(stdout);
  return ts;
}


//! Remove all space characters from a string.
/*!
 * \param text reference to the string
 * \return reference to the in-place-modified string
 */
QString &
unspacifyString(QString &text)
{
  if(text.isEmpty())
    {
      return text;
    }

  text.remove(QRegExp("\\s+"));

  return text;
}


//! Produce a binary representation of an int
/*!
 * \param value int value
 * \return a QString with the value binary representation
 */
QString
binaryRepresentation(int value)
{
  QString string;
  string = QString("%1").arg(value, 32, 2);

  return string;
}


//! Produce a shortened version of a string.
/*!
 * Often, it is necessary to display in a graphical user interface a very long
 * string that cannot fit in the provided widget. This function returns a
 * shortened version of the input string.
 *
 * For example, "Interesting bits of information are often lost where there are
 * too many details", becomes "Interes...details".
 *
 * \param text reference to the string to be shortened
 * \param charsLeft number of characters to be left at the left of the
 * elision string
 * \param charsRight number of characters to be left at the right of the
 * elision string
 * \param delimitor string to use as the elision delimitor
 */
QString
elideText(const QString &text,
          int charsLeft,
          int charsRight,
          const QString &delimitor)
{
  // We want to elide text. For example, imagine we have text = "that
  // is beautiful stuff", with charsLeft 4 and charsRight 4 and
  // delimitor "...". Then the result would be "that...tuff"

  if(charsLeft < 1 || charsRight < 1)
    {
      return text;
    }

  int size = text.size();

  // If the text string is already too short, no need to elide
  // anything.
  if((charsLeft + charsRight + delimitor.size()) >= size)
    {
      return text;
    }

  QString result = text.left(charsLeft);
  result.append(delimitor);
  result.append(text.right(charsRight));

  return result;
}


//! Convert a very long text to a paragraph.
/*!
 * When a text string is too long to be displayed in a line of reasonable
 * length, insert newline characters at positions calculated to yield a
 * paragraph of a given width.
 *
 * The text is left-justified with the most fitting line size possible.
 *
 * \param text string to be converted to a paragraph (a stanza)
 * \param width width in characters of the stanza
 * \return a new string with the stanzified text
 */
QString
stanzify(const QString &text, int width)
{
  QString result = text;

  // First, replace all the existing newline chars with spaces.

  result = result.replace("\n", " ");

  int iter = width;

  // Then, iterate in the obtained string and every width characters try to
  // insert a newline character by iterating back to the left and searching
  // for a space.

  for(; iter < result.size(); iter += width)
    {
      // Now iterate in reverse and search for a space where to insert a
      // newline

      int jter = iter;

      for(; jter >= 0; --jter)
        {
          if(result.at(jter) == ' ')
            {
              result[jter] = '\n';
              break;
            }
        }
    }

  return result;
}


//! Convert a very long text to paragraphs.
/*!
 * When a text string is too long to be displayed in a line of reasonable
 * length, insert newline characters at positions calculated to yield a
 * paragraph of a given width.
 *
 * The text is left-justified with the most fitting line size possible.
 *
 * \param text string to be converted to a paragraph (a stanza)
 * \param width width in characters of the stanza
 * \return a new string with the stanzified text
 */
QString
stanzifyParagraphs(const QString &text, int width)
{
  QString result;

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

  for(int iter = 0; iter < paragraphList.size(); ++iter)
    {
      QString line = paragraphList.at(iter);

      QString stanzifiedLine = stanzify(line, width);

      result.append(stanzifiedLine);
      result.append("\n");
    }

  return result;
}


//! Determine the number of zero decimals between the decimal point and the
//! first non-zero decimal.
/*!
 * 0.11 would return 0 (no empty decimal)
 * 2.001 would return 2
 * 1000.0001254 would return 3
 *
 * \param value the value to be analyzed
 * \return the number of '0' decimals between the decimal separator '.' and
 * the first non-0 decimal
 */
int
zeroDecimals(double value)
{

  int intPart = static_cast<int>(value);

  double decimalPart = value - intPart;

  int count = -1;

  while(1)
    {
      ++count;

      decimalPart *= 10;

      if(decimalPart > 1)
        return count;
    }

  return count;
}


//! Return the delta corresponding to m/z \p value and \p ppm part-per-million
double
ppm(double value, double ppm)
{

  return (value * ppm) / 1000000;
}


//! Return \p value incremented with the matching \p ppm part.
double
addPpm(double value, double ppm)
{

  return value + (value * ppm) / 1000000;
}


//! Return \p value decremented with the matching \p ppm part.
double
removePpm(double value, double ppm)
{

  return value - (value * ppm) / 1000000;
}


//! Return the delta corresponding to m/z \p value and resolution \p res
double
res(double value, double res)
{
  return value / res;
}


//! Return \p value incremented with the matching \p res part.
double
addRes(double value, double res)
{
  return value + (value / res);
}

//! Return \p value decremented with the matching \p res part.
double
removeRes(double value, double res)
{
  return value - (value / res);
}


//! Craft a string representing the address location (for debugging).
QString
pointerAsString(void *pointer)
{
  return QString("%1").arg(
    (quintptr)pointer, QT_POINTER_SIZE * 2, 16, QChar('0'));
}


} // namespace msXpS
