/* 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 <math.h>


/////////////////////// Qt includes
#include <QDir>
#include <QString>
#include <QStandardPaths>


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


/////////////////////// Local includes
#include "globals.hpp"


namespace MsXpS
{

namespace libXpertMass
{

/*!
\enum MsXpS::libXpertMass::MassType
\ingroup Globals

This enum type specifies the type of mass:

\value MASS_NONE
     The mass type is not defined
\value MASS_MONO
     The mass is monoisotopic
\value MASS_AVG
     The mass is average
\value MASS_BOTH
     (MASS_MONO | MASS_AVG)
*/

/*!
\enum MsXpS::libXpertMass::MassToleranceType
\ingroup Globals

  This enum type specifies the kind of mass tolerance to use for a mass
calculation or a mass comparison.

  \value MASS_TOLERANCE_NONE
         The tolerance is not specified
  \value MASS_TOLERANCE_PPM
         The tolerance is based on parts per million
  \value MASS_TOLERANCE_MZ
         The tolerance is based on an absolute m/z value
  \value MASS_TOLERANCE_AMU
         The tolerance is based on an absolute mass value
  \value MASS_TOLERANCE_RES
         The tolerance is based on resolution
  \omitvalue MASS_TOLERANCE_LAST
*/

/*!
\brief Map relating the \l{MassToleranceType} to a textual representation
\ingroup Globals
*/
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"}};


/*!
\enum MsXpS::libXpertMass::PolymerEnd
\ingroup Globals

This enum specifies the polymer end.

\value END_NONE
       Not defined

\value END_LEFT
       The left end

\value END_RIGHT
       The right end

\value END_BOTH
       (END_LEFT | END_RIGHT)
*/

/*!
\enum MsXpS::libXpertMass::SelectionType
\ingroup Globals

This enum specifies the selection type in a polymer sequence.

\value SELECTION_TYPE_RESIDUAL_CHAINS
       The selection comprises only residues

\value SELECTION_TYPE_OLIGOMERS
       The selection comprises oligomers, that is, residual chains capped with
the left and right caps.
*/

/*!
\enum MsXpS::libXpertMass::CapType
\ingroup Globals

This enum specifies the type of cap (the chemical entities that are set to
the polymer ends so as to finish its polymerization state from a chain of
residues to an actual polymer molecule.

\value CAP_NONE
       The cap is not defined

\value CAP_LEFT
       The left cap

\value CAP_RIGHT
       The right cap

\value CAP_BOTH
       (CAP_LEFT | CAP_RIGHT)
*/

/*!
\enum MsXpS::libXpertMass::MonomerChemEnt
\ingroup Globals

This enum specifies the monomer chemical entities to account for in a
calculation.

This enum is typically used when mass calculations need to account or not for
the various chemical entities that are attached to a given monomer.

\value MONOMER_CHEMENT_NONE
       The monomer chemical entity is not defined.

\value MONOMER_CHEMENT_MODIF
       The monomer modifications

\value MONOMER_CHEMENT_CROSS_LINK
       The monomer cross-links
*/

/*!
\enum MsXpS::libXpertMass::PolymerChemEnt
\ingroup Globals

This enum specifies the polymer sequence chemical entities to account for in a
calculation.


This enum is typically used when mass calculations need to account or not for
the various chemical entities that are attached to a given polymer.

\value POLYMER_CHEMENT_NONE
       The polymer chemical entity is not defined.

\value POLYMER_CHEMENT_LEFT_END_MODIF
       The left end modification

\value POLYMER_CHEMENT_FORCE_LEFT_END_MODIF
       The left end modification, even if that polymer's end is not selected


\value POLYMER_CHEMENT_RIGHT_END_MODIF
       The right end modification

\value POLYMER_CHEMENT_FORCE_RIGHT_END_MODIF
       The right end modification, even if that polymer's end is not
selected

\value POLYMER_CHEMENT_BOTH_END_MODIF
       (POLYMER_CHEMENT_LEFT_END_MODIF | POLYMER_CHEMENT_RIGHT_END_MODIF)

\value POLYMER_CHEMENT_FORCE_BOTH_END_MODIF
       (POLYMER_CHEMENT_FORCE_LEFT_END_MODIF |
POLYMER_CHEMENT_FORCE_RIGHT_END_MODIF)
*/


/*!
\enum MsXpS::libXpertMass::HashAccountData
\ingroup Globals

This enum specifies the chemical entites to account for when calculating a hash.

\value HASH_ACCOUNT_SEQUENCE
       The sequence

\value HASH_ACCOUNT_MONOMER_MODIF
       Monomer modifications

\value HASH_ACCOUNT_POLYMER_MODIF
       Polymer modifications
*/


/*!
  \variable MsXpS::libXpertMass::subFormulaRegExp

  \brief Regular expression used to deconstruct the main formula into
minus and plus component subformulas.

  \code
  "([+-]?)([A-Z][a-z]*)(\\d*[\\.]?\\d*)"
  \endcode

  \sa Formula::splitActionParts()
*/
QRegularExpression subFormulaRegExp =
  QRegularExpression(QString("([+-]?)([A-Z][a-z]*)(\\d*[\\.]?\\d*)"));

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

/*!
\brief Regular expression that matches the m/z,i pairs in text files.
\ingroup Globals
*/
QRegularExpression gXyFormatMassDataRegExp =
  QRegularExpression("^(\\d*\\.?\\d+)([^\\d^\\.^-]+)(-?\\d*\\.?\\d*[e-]?\\d*)");

/*!
\brief Number of decimal places after the decimal symbol for atom masses.
\ingroup Globals
 */
int ATOM_DEC_PLACES = 10;

/*!
\brief Number of decimal places after the decimal symbol for oligomer masses.
\ingroup Globals
 */
int OLIGOMER_DEC_PLACES = 5;

/*!
\brief Number of decimal places after the decimal symbol for polymer masses.
\ingroup Globals
 */
int POLYMER_DEC_PLACES = 3;

/*!
\brief Number of decimal places after the decimal symbol for pH/pKa values.
\ingroup Globals
 */
int PH_PKA_DEC_PLACES = 2;


/*!
\brief Returns the average of the \a list of \c double values.
\ingroup Globals

All the values in list are process in order to compute the following:

\list
\li \a sum
\li \a average
\li \a variance
\li \a stdDeviation
\li \a nonZeroSmallest
\li \a smallest
\li \a smallestMedian
\li \a greatest
\endlist

statistical values if the corresponding parameters are non-nullptr.

\sa doubleVectorStatistics()
*/
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.
  std::sort(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 = std::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);
    }
}


/*!
\brief Returns the average of the \a vector of \c double values.
\ingroup Globals

All the values in list are process in order to compute the following:

\list
\li \a sum
\li \a average
\li \a variance
\li \a stdDeviation
\li \a nonZeroSmallest
\li \a smallest
\li \a smallestMedian
\li \a greatest
\endlist

statistical values if the corresponding parameters are non-nullptr.

\sa doubleListStatistics()
*/
void
doubleVectorStatistics(std::vector<double> &vector,
                       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("Programming error.");

  // Sort the vector, we'll need it sorted to compute the median value.
  std::sort(vector.begin(), vector.end());

  int count = vector.size();

  if(!count)
    return;

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

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

  for(auto &&value : vector)
    {
      double current = value;
      *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(auto &&value : vector)
    {
      varN += (value - *average) * (value - *average);
    }

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

  // Now the median value

  // qDebug() << "count:" << count;

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


/*!
\brief Returns true if both double values \a value1 and \a value2, are equal
within the double representation capabilities of the platform, false otherwise.
\ingroup Globals

In the comparison, the \a decimal_places are taken into account.
*/
bool
almostEqual(double value1, double value2, int decimal_places)
{
  // 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 * decimal_places;
  // 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;
}


/*!
 \brief Provides a text stream handle to the standard output.
\ingroup Globals

 Use:

 \code
 qStdOut() << __FILE__ << __LINE__
                         << "text to the standard output,"
                         << "not the error standard output."
 \endcode

 Returns a reference to a static QTextStream that directs to the standard
 output.
 */
QTextStream &
qStdOut()
{
  static QTextStream ts(stdout);
  return ts;
}


/*!
\brief Removes all space characters from the \a text string (the removal is
in-place).
\ingroup Globals

Returns a reference to the in-place-modified string.
 */
QString &
unspacifyString(QString &text)
{
  if(text.isEmpty())
    {
      return text;
    }

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

  return text;
}


/*!
\brief Returns a string with a binary representation of the \a value integer.
\ingroup Globals
*/
QString
binaryRepresentation(int value)
{
  QString string;
  string = QString("%1").arg(value, 32, 2);

  return string;
}


/*!
\brief Returns a shortened (elided) version of the \a text string.
\ingroup Globals

It is sometimes 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".

\a chars_left: Count of characters to be kept on the left side of the string.

\a chars_right: Count of characters to be kept on the right side of the string.

\a delimiter string to use as the elision delimiter (in the above example, that
is '.').
 */
QString
elideText(const QString &text,
          int chars_left,
          int chars_right,
          const QString &delimiter)
{
  // 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(chars_left < 1 || chars_right < 1)
    {
      return text;
    }

  int size = text.size();

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

  QString result = text.left(chars_left);
  result.append(delimiter);
  result.append(text.right(chars_right));

  return result;
}


/*!
\brief Returns a string containing a paragraph-based version of the very long
single-line \a text.
\ingroup Globals

When a text string is too long to be displayed in a line of reasonable
length, inserts newline characters at positions calculated to yield a
paragraph of the given \a width.

\sa stanzifyParagraphs()
 */
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;
}


/*!
\brief Returns a string containing a series of paragraph-based versions of the
very long single-line-containing paragraphs in \a text.
\ingroup Globals

\a text is a string with newline characters that delimit paragraph that
thelmselves are made of a very long single line. This function splits \a text
along the \c '\n' character and each obtained very long single-line string is
reduced to a set of lines to make a pararagraph (see \l{stanzify}). The with of
the line in that generated paragraph is \a width.

\sa stanzify()
 */
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;
}


/*!
\brief Returns the number of zero decimals in \a value that are found between
the decimal point and the first non-zero decimal.
\ingroup Globals

For example, 0.11 would return 0 (no empty decimal)

2.001 would return 2

1000.0001254 would return 3
*/
int
countZeroDecimals(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;
}


/*!
\brief Returns the delta value corresponding to \a value and \a ppm
part-per-million.
\ingroup Globals
*/
double
ppm(double value, double ppm)
{

  return (value * ppm) / 1000000;
}


/*!
\brief Returns \a value incremented by the matching \a ppm part.
\ingroup Globals
*/
double
addPpm(double value, double ppm)
{

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


/*!
\brief Returns \a value decremented by the matching \a ppm part.
\ingroup Globals
*/
double
removePpm(double value, double ppm)
{
  return value - (value * ppm) / 1000000;
}


/*!
\brief Return the delta corresponding to \a value and resolution \a res.
\ingroup Globals
*/
double
res(double value, double res)
{
  return value / res;
}


/*!
\brief Returns \a value incremented by the matching \a res part.
\ingroup Globals
*/
double
addRes(double value, double res)
{
  return value + (value / res);
}

/*!
\brief Returns \a value decremented by the matching \a res part.
\ingroup Globals
*/
double
removeRes(double value, double res)
{
  return value - (value / res);
}


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

/*!
\brief Returns a string holding the the file path to the configuration settings
for application \a module_name.
\ingroup Globals
  */
QString
configSettingsFilePath(const QString &module_name)
{

  // The configuration settings file for all the settings of the program

  QString file_path =
    QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);

  if(file_path.isEmpty())
    file_path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);

  file_path = file_path + QDir::separator() + module_name;

  file_path = file_path + QDir::separator() + "configSettings.ini";

  return file_path;
}


} // namespace libXpertMass

} // namespace MsXpS
