/*******************************************************************************
 * Copyright (c) 2015 Olivier Langella <Olivier.Langella@moulon.inra.fr>.
 *
 * This file is part of the PAPPSOms++ library.
 *
 *     PAPPSOms++ 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.
 *
 *     PAPPSOms++ 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 PAPPSOms++.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Contributors:
 *     Olivier Langella <Olivier.Langella@moulon.inra.fr> - initial API and
 *implementation
 ******************************************************************************/

/////////////////////// StdLib includes
#include <cmath>
#include <iomanip>


/////////////////////// Qt includes
#include <QDebug>
#include <QFile>
#include <QTextStream>


/////////////////////// Local includes
#include "config.h"
#include "utils.h"
#include "types.h"
#include "exception/exceptionnotfound.h"
#include "trace/trace.h"


namespace pappso
{

// Matches a double (decimal value, that is, m/z
// value)
QRegularExpression Utils::unsignedDoubleNumberNoExponentialRegExp =
  QRegularExpression("\\d*\\.?\\d+");

// Matches anything that is not digit, '.', or '-'
// (that is, matches a separator
QRegularExpression Utils::anythingButDigitDotDash =
  QRegularExpression("[^\\d^\\.^-]+");

// Matches a double (with exp notation
// possibly) and also potentially a '-' sign. This is the intensity.
QRegularExpression Utils::signedDoubleNumberExponentialRegExp =
  QRegularExpression("-?\\d*\\.?\\d*[e-]?\\d*");

// Matches <number><separator><number>, that is: m/z<separator>intensity.
QRegularExpression Utils::xyMassDataFormatRegExp =
  // QRegularExpression("^(\\d*\\.?\\d+)([^\\d^\\.^-]+)(-?\\d*\\.?\\d*[e-]?\\d*)");
  QRegularExpression(
    QString("^(%1)(%2)(%3)")
      .arg(Utils::unsignedDoubleNumberNoExponentialRegExp.pattern())
      .arg(Utils::anythingButDigitDotDash.pattern())
      .arg(Utils::signedDoubleNumberExponentialRegExp.pattern()));


QRegularExpression Utils::endOfLineRegExp = QRegularExpression("^\\s+$");

const QString
Utils::getLexicalOrderedString(unsigned int num)
{
  int size = log10(num);
  size += 97;
  QLatin1Char latin1_char(size);
  QString base(latin1_char);
  base.append(QString().setNum(num));
  return (base);
}


void
Utils::writeLexicalOrderedString(QTextStream *p_out, unsigned int num)
{
  *p_out << (char)(log10(num) + 97) << num;
}


//! 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
Utils::zeroDecimalsInValue(pappso_double value)
{
  // qDebug() << qSetRealNumberPrecision(10) << "Double value: " << value;

  int intPart = static_cast<int>(value);

  // qDebug() << "int part:" << intPart;

  double decimalPart = value - intPart;

  // qDebug() << qSetRealNumberPrecision(10) << "decimal part: " << decimalPart;

  int count = 0;

  while(decimalPart > 0)
    {
      ++count;

      decimalPart *= 10;

      // qDebug() << "Iteration " << count << "decimal part:" << decimalPart;

      if(decimalPart >= 1)
        {
          // qDebug() << "Because decimal part " << decimalPart
          //<< "is >= 1, breaking loop while count is " << count << ".";

          break;
        }
    }

  // qDebug() << "Returning count:" << count - 1;

  return count - 1;
}


pappso_double
Utils::roundToDecimals(pappso_double value, int decimal_places)
{
  if(decimal_places < 0)
    return value;

  return ceil((value * pow(10, decimal_places)) - 0.49) /
         pow(10, decimal_places);
}


long long int
Utils::roundToDecimal32bitsAsLongLongInt(pappso::pappso_double input)
{
  pappso::pappso_double test_decimal = 100000000000;
  if(sizeof(int *) == 4)
    { // 32bits
      test_decimal = 100000000;
    }
  return (floor(input * test_decimal));
}


std::string
Utils::toUtf8StandardString(const QString &text)
{
  std::string env_backup;
  try
    {
#ifdef MXE
      // std::locale::global(std::locale("C")); // set locale to default locale
      env_backup = std::setlocale(LC_ALL, nullptr);
      std::setlocale(LC_ALL, "C");
#else
      std::locale::global(std::locale("C")); // set locale to default locale
#endif
    }
  catch(std::exception &error)
    {
      throw pappso::PappsoException(
        QObject::tr("Error trying to set local to C : %1").arg(error.what()));
    }
  // Now perform the conversion.
  QByteArray byte_array = text.toUtf8();
  std::string stdText   = "";

  for(char c : byte_array)
    {
      stdText += c;
    }

  try
    {
#ifdef MXE
      // std::locale::global(std::locale("C")); // set locale to default locale
      std::setlocale(LC_ALL, env_backup.c_str());
#else // Set back the locale to the backed-up one.
      std::locale::global(
        std::locale("")); // sets locale according to OS environment
#endif
    }
  catch(std::exception &error)
    {

      throw pappso::PappsoException(
        QObject::tr("Error trying to set local to original system one %1 : %2")
          .arg(env_backup.c_str())
          .arg(error.what()));
    }

  return stdText;
}


bool
Utils::writeToFile(const QString &text, const QString &file_name)
{

  QFile file(file_name);

  if(file.open(QFile::WriteOnly | QFile::Truncate))
    {

      QTextStream out(&file);

      out << text;

      out.flush();
      file.close();

      return true;
    }

  return false;
}


bool
Utils::appendToFile(const QString &text, const QString &file_name)
{

  QFile file(file_name);

  if(file.open(QFile::WriteOnly | QFile::Append))
    {

      QTextStream out(&file);

      out << text;

      out.flush();
      file.close();

      return true;
    }

  return false;
}


std::size_t
Utils::extractScanNumberFromMzmlNativeId(const QString &spectrum_native_id)
{
  qDebug() << " " << spectrum_native_id;
  QStringList native_id_list = spectrum_native_id.split("=");
  if(native_id_list.size() < 2)
    {
      throw ExceptionNotFound(
        QObject::tr("scan number not found in mzML native id %1")
          .arg(spectrum_native_id));
    }
  else
    {
      /** TODO activate this in a future release to ensure scan number
        for(auto i = 0; i < native_id_list.size(); i += 2)
        {
        if(native_id_list[i] == "scan")
        {
        return native_id_list[i + 1].toULong();
        }
        }

        throw ExceptionNotFound(
        QObject::tr("scan number not found in mzML native id %1")
        .arg(spectrum_native_id));

*/
      return native_id_list.back().toULong();
    }
  return 0;
}


QString
Utils::pointerToString(const void *const pointer)
{
  return QString("%1").arg(
    (quintptr)pointer, QT_POINTER_SIZE * 2, 16, QChar('0'));
}


//! Tell if both double values, are equal within the double representation
//! capabilities of the platform.
bool
Utils::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;
}


double
Utils::nearestGreater(double value)
{
  return std::nextafter(value, value + 1);
}


QString
Utils::chronoTimePointDebugString(
  const QString &msg, std::chrono::system_clock::time_point chrono_time)
{

  time_t tt;

  tt = std::chrono::system_clock::to_time_t(chrono_time);

  QString debug_text =
    QString("%1 - %2\n").arg(msg).arg(QString::fromLatin1(ctime(&tt)));

  return debug_text;
}


QString
Utils::chronoIntervalDebugString(
  const QString &msg,
  std::chrono::system_clock::time_point chrono_start,
  std::chrono::system_clock::time_point chrono_finish)
{
  QString debug_text =
    QString(
      "%1 %2 min = %3 s = %4 ms = %5 "
      "µs\n")
      .arg(msg)
      .arg(std::chrono::duration_cast<std::chrono::minutes>(chrono_finish -
                                                            chrono_start)
             .count())
      .arg(std::chrono::duration_cast<std::chrono::seconds>(chrono_finish -
                                                            chrono_start)
             .count())
      .arg(std::chrono::duration_cast<std::chrono::milliseconds>(chrono_finish -
                                                                 chrono_start)
             .count())
      .arg(std::chrono::duration_cast<std::chrono::microseconds>(chrono_finish -
                                                                 chrono_start)
             .count());

  return debug_text;
}


std::vector<double>
Utils::splitMzStringToDoubleVectorWithSpaces(const QString &text,
                                             std::size_t &error_count)
{

  QStringList string_list =
    text.split(QRegularExpression("[\\s]+"), Qt::SkipEmptyParts);

  // qDebug() << "string list:" << string_list;

  std::vector<double> double_vector;

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

      bool ok = false;

      double current_double = current_string.toDouble(&ok);

      if(!current_double && !ok)
        {
          ++error_count;
          continue;
        }

      double_vector.push_back(current_double);
    }

  return double_vector;
}


std::vector<std::size_t>
Utils::splitSizetStringToSizetVectorWithSpaces(const QString &text,
                                               std::size_t &error_count)
{
  // qDebug() << "Parsing text:" << text;

  QStringList string_list =
    text.split(QRegularExpression("[\\s]+"), Qt::SkipEmptyParts);

  // qDebug() << "string list size:" << string_list.size()
  //<< "values:" << string_list;

  std::vector<std::size_t> sizet_vector;

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

      bool ok = false;

      std::size_t current_sizet = current_string.toUInt(&ok);

      if(!current_sizet && !ok)
        {
          ++error_count;
          continue;
        }

      sizet_vector.push_back(current_sizet);
    }

  return sizet_vector;
}
QString
Utils::booleanToString(bool value)
{
  if(value)
    return "TRUE";
  return "FALSE";
}

QString
Utils::msDataFormatAsString(MsDataFormat mz_format)
{

  if(mz_format == MsDataFormat::mzML)
    return "mzML";
  else if(mz_format == MsDataFormat::mzXML)
    return "mzXML";
  else if(mz_format == MsDataFormat::MGF)
    return "MGF";
  else if(mz_format == MsDataFormat::SQLite3)
    return "SQLite3";
  else if(mz_format == MsDataFormat::xy)
    return "xy";
  else if(mz_format == MsDataFormat::mz5)
    return "mz5";
  else if(mz_format == MsDataFormat::msn)
    return "msn";
  else if(mz_format == MsDataFormat::abSciexWiff)
    return "abSciexWiff";
  else if(mz_format == MsDataFormat::abSciexT2D)
    return "abSciexT2D";
  else if(mz_format == MsDataFormat::agilentMassHunter)
    return "agilentMassHunter";
  else if(mz_format == MsDataFormat::thermoRaw)
    return "thermoRaw";
  else if(mz_format == MsDataFormat::watersRaw)
    return "watersRaw";
  else if(mz_format == MsDataFormat::brukerFid)
    return "brukerFid";
  else if(mz_format == MsDataFormat::brukerYep)
    return "brukerYep";
  else if(mz_format == MsDataFormat::brukerBaf)
    return "brukerBaf";
  else if(mz_format == MsDataFormat::brukerTims)
    return "brukerTims";
  else if(mz_format == MsDataFormat::brukerBafAscii)
    return "brukerBafAscii";
  else
    return "unknown";
}


QString
Utils::fileReaderTypeAsString(FileReaderType file_reader_type)
{

  if(file_reader_type == FileReaderType::pwiz)
    return "pwiz";
  else if(file_reader_type == FileReaderType::xy)
    return "xy";
  else if(file_reader_type == FileReaderType::tims)
    return "tims";
  else if(file_reader_type == FileReaderType::tims_frames)
    return "tims_frames";
  else
    return "unknown";
}

QString
Utils::toString(specglob::SpectralAlignmentType type)
{
  switch(type)
    {
      case specglob::SpectralAlignmentType::align:
        return "AL";
        break;
      case specglob::SpectralAlignmentType::nonAlign:
        return "NA";
        break;
      case specglob::SpectralAlignmentType::reAlign:
        return "RA";
        break;
      default:
        return "ER";
    }
}

QString
Utils::toString(specglob::ExperimentalSpectrumDataPointType type)
{
  switch(type)
    {
      case specglob::ExperimentalSpectrumDataPointType::both:
        return "both";
        break;
      case specglob::ExperimentalSpectrumDataPointType::native:
        return "native";
        break;
      case specglob::ExperimentalSpectrumDataPointType::symmetric:
        return "symmetric";
        break;
      default:
        return "synthetic";
    }
}

QString
Utils::getVersion()
{
  QString version(PAPPSOMSPP_VERSION);
  return version;
}
} // namespace pappso
