/* BEGIN software license
 *
 * MsXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright (C) 2009--2024 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>
#include <cstdio>


////////////////////////////// Qt includes
#include <QDebug>
#include <QStandardPaths>
#include <QDir>
#include <QProcess>


////////////////////////////// Local includes
#include "MsXpS/libXpertMassCore/Utils.hpp"

namespace MsXpS
{
namespace libXpertMassCore
{


/*!
\class MsXpS::libXpertMassCore::Utils
\inmodule libXpertMassCore
\ingroup XpertMassCoreUtilities
\inheaderfile Utils.hpp

\brief The Utils class provides a number of utilitary features that may be of
use anywhere in the XpertMass source code tree.
*/


/*!
\variable MsXpS::libXpertMassCore::Utils::subFormulaRegExp

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

This regular expression does not account for the \"<text>\" title
of the actionformula.

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

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

/*!
\variable MsXpS::libXpertMassCore::Utils::xyFormatMassDataRegExp

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

/*!
\variable MsXpS::libXpertMassCore::Utils::endOfLineRegExp

\brief Regular expression that matches the end of line in text files.
*/
QRegularExpression Utils::endOfLineRegExp = QRegularExpression("^\\s+$");


/*!
\variable MsXpS::libXpertMassCore::Utils::xmlIndentationToken

\brief String used to craft the indentation of the XML elements.
*/
QString Utils::xmlIndentationToken = QString(" ");

/*!
\variable MsXpS::libXpertMassCore::Utils::configSettingsFilePath

\brief String holding the QSettings-based configuration file path as set
by the caller (the program linking to this library).
*/
QString MsXpS::libXpertMassCore::Utils::configSettingsFilePath;

/*!
\brief Constructs a Utils instance setting parent to \a parent.
*/
Utils::Utils(QObject *parent): QObject(parent)
{
}

/*!
\brief Destructs a Utils instance.
*/
Utils::~Utils()
{
}

/*!
\brief Configures the format of all the messages that are output using qDebug(),
qDebug(), qDebug(), qFatal() and qDebug() using a number of
parameters.

\list
\li \a type The kind of message

\li \a context The context of the message

\li \a msg The message to output.
\endlist
*/
void
Utils::messageOutputFormat(QtMsgType type,
                           const QMessageLogContext &context,
                           const QString &msg)
{
// Define ANSI color codes
#define RESET   "\033[0m"
#define BLACK   "\033[30m"
#define RED     "\033[31m"
#define GREEN   "\033[32m"
#define YELLOW  "\033[33m"
#define BLUE    "\033[34m"
#define MAGENTA "\033[35m"
#define CYAN    "\033[36m"
#define WHITE   "\033[37m"
#define BOLD    "\033[1m"

  QByteArray localMsg  = msg.toLocal8Bit();
  const char *file     = context.file ? context.file : "";
  const char *function = context.function ? context.function : "";

  QString prefix;
  QString color;


  switch(type)
    {
      case QtInfoMsg:
        prefix = "INFO: ";
        color  = QString("%1").arg(GREEN);
        break;
      case QtWarningMsg:
        prefix = "WARNING: ";
        color  = QString("%1").arg(BLUE);
        break;
      case QtCriticalMsg:
        prefix = "CRITICAL: ";
        color  = QString("%1").arg(MAGENTA);
        break;
      case QtFatalMsg:
        prefix = "FATAL: ";
        color  = QString("%1").arg(RED);
        break;
      case QtDebugMsg:
        prefix = "DEBUG: ";
        color  = QString("%1").arg(YELLOW);
        break;
    }

  fprintf(stderr,
          "%s%s%s%s:%s%u%s\n%s\n%s======> %s%s\n\n",
          color.toLocal8Bit().constData(),
          prefix.toLocal8Bit().constData(),
          RESET,
          file,
          color.toLocal8Bit().constData(),
          context.line,
          RESET,
          function,
          color.toLocal8Bit().constData(),
          localMsg.constData(),
          RESET);
}

/*!
\brief Installs the message handler.
\sa Utils::messageOutputFormat()
*/
void
Utils::configureDebugMessagesFormat()
{
  qInstallMessageHandler(messageOutputFormat);
}

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

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 std_dev
\li \a smallest_non_zero
\li \a smallest
\li \a smallest_median
\li \a greatest
\endlist

statistical values if the corresponding parameters are non-nullptr.

\sa doubleVectorStatistics()
*/
void
Utils::doubleListStatistics(QList<double> list,
                            double &sum,
                            double &average,
                            double &variance,
                            double &std_dev,
                            double &smallest_non_zero,
                            double &smallest,
                            double &smallest_median,
                            double &greatest)
{
  // 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;
  smallest_non_zero = 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 < smallest_non_zero)
        smallest_non_zero = 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;
  std_dev  = std::sqrt(variance);

  // Now the median value

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

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

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

All the values in the container are processed in order to compute the following:

\list
\li \a sum
\li \a average
\li \a variance
\li \a std_dev
\li \a smallest_non_zero
\li \a smallest
\li \a smallest_median
\li \a greatest
\endlist

statistical values if the corresponding parameters are non-nullptr.

\sa doubleListStatistics()
*/
void
Utils::doubleVectorStatistics(std::vector<double> &vector,
                              double &sum,
                              double &average,
                              double &variance,
                              double &std_dev,
                              double &smallest_non_zero,
                              double &smallest,
                              double &smallest_median,
                              double &greatest)
{
  // 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;
  smallest_non_zero = std::numeric_limits<double>::max();

  for(double &value : vector)
    {
      double current  = value;
      sum            += current;

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

  average = sum / count;

  double varN = 0;

  for(auto &value : vector)
    {
      varN += (value - average) * (value - average);
    }

  variance = varN / count;
  std_dev  = std::sqrt(variance);

  // Now the median value

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

  if(count == 1)
    {
      smallest_median = vector.front();
    }
  else
    {
      if(count % 2 == 0)
        smallest_median = (vector.at(count / 2 - 1) + vector.at(count / 2)) / 2;
      else
        smallest_median = 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.

In the comparison, the \a decimal_places are taken into account.
*/
bool
Utils::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);

  // qDebug() << __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 sum_of_values = std::abs(value1 + value2);
  // QString sum_of_valuesString = QString("%1").arg(sum_of_values,
  // 0, 'f', 60);

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

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

  double scale_factor = epsilon * sum_of_values * decimal_places;
  // QString scale_factorString = QString("%1").arg(scale_factor,
  // 0, 'f', 60);

  // qDebug() << "diff_of_values:" << diff_of_valuesString << "sum_of_values:"
  // << sum_of_valuesString << "epsilon:" << epsilonString << "scale_factor:" <<
  // scale_factorString;

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

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

  return res;
}

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

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 &
Utils::qStdOut()
{
  static QTextStream ts(stdout);
  return ts;
}

/*!
\brief Removes all space characters from a copy of the \a text string and
returns the new string.
*/
QString
Utils::unspacify(const QString &text)
{
  if(text.isEmpty())
    {
      return text;
    }

  QString unspacified = text;
  unspacified.remove(QRegularExpression("\\s+"));

  return unspacified;
}

/*!
\brief Returns a string with a binary representation of the \a value integer.
*/
QString
Utils::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.

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
Utils::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.

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.

Newline characters can be forced in the output stanza if they appear as
""{[\n]}" in the initial text.

This function is typically used to stanzify tooltip text strings.

\sa stanzifyParagraphs()
*/
QString
Utils::stanzify(const QString &text, int width)
{
  // qDebug() << "The text to stanzify:" << text;

  QString purified = text;

  // First, replace all the existing newline chars with spaces.
  // Note that when {[\n]} is found in the text, the newline is conserved.

  purified = purified.replace("\n", " ");
  // The '\n' inside the{[]} has been replaced by ' ', so we need to put ' '
  // in that "{[ ]}" string!
  purified = purified.replace("{[ ]}", "\n");
  // No more than one space, please.
  purified = purified.replace("  ", " ");

  // qDebug() << "Purified text to stanzify:" << purified;

  // "Number of points that will make the peak shape. \nTypically, an optimal
  // value is in the 150 to 200 range"

  int line_iter = 0;


  for(int iter = 0; iter < purified.size(); ++iter, ++line_iter)
    {
      // qDebug() << "Iterating into iter:" << iter
      //          << "which corresponds to text:" << purified.first(iter + 1)
      //          << "line_iter:" << line_iter;

      if(purified.at(iter) == '\n')
        {
          // qDebug() << "Found a newline, resetting line_iter to -1 so that at
          // "
          //             "next iteration it will be 0.";
          line_iter = -1;
          continue;
        }

      if(line_iter >= width - 1)
        {
          // qDebug() << "Reached the max index of " << width - 1
          //          << "need to find a space by looking back in the string.";

          --iter;

          // qDebug() << "Back-iterated once and char is:" << purified.at(iter);

          while(purified.at(iter) != ' ' && purified.at(iter) != '\n')
            {
              --iter;
              // qDebug() << "Back-iterated once and char is:"
              //          << purified.at(iter);
            }

          // qDebug() << "At iter" << iter
          //          << "found a space char. Replacing it with a newline.";

          purified.replace(iter, 1, '\n');

          // qDebug() << "Resetting line_iter to -1 so that at next iteration it
          // "
          //             "will be 0.";

          line_iter = -1;
        }
    }

  // qDebug() << "Returning stanzified text:" << purified;

  return purified;
}

/*!
 * \brief Returns a string containing a paragraph-based version of the very long
 * single-line \a text.
 *
 * 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
Utils::stanzifyOld(const QString &text, int width)
{
  QString result = text;

  // First, replace all the existing newline chars with spaces.
  // Note that when {[\n]} is found in the text, the newline is conserved.

  result = result.replace("\n", " ");
  result = result.replace("{[ ]}", "\n");
  // No more than one space, please.
  result = result.replace("  ", " ");

  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

      // If we encounter a newline, keep it and continue;

      if(result.at(iter) == "\n")
        continue;

      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.

\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
Utils::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.

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

2.001 would return 2

1000.0001254 would return 3
*/
int
Utils::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.
*/
double
Utils::ppm(double value, double ppm)
{

  return (value * ppm) / 1000000;
}

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

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

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

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

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

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

/*!
\fn template <typename T> QString Utils::pointerAsString(T *ptr)

\brief Returns a string representing the \a ptr address location.

\sa stringAsPointer
*/
template <typename T>
QString
Utils::pointerAsString(T *ptr)
{
  return QString::number(reinterpret_cast<quintptr>(ptr),
                         16); // Convert to hex string
}

/*!
\fn template <typename T> T * Utils::stringAsPointer(const QString &str)

\brief Returns a pointer to type T corresponding the \a str representation of
the pointer.

\sa pointerAsString()
*/
template <typename T>
T *
Utils::stringAsPointer(const QString &str)
{
  bool ok;
  quintptr intPtr =
    str.toULongLong(&ok, 16); // Convert hex string back to integer
  return ok ? reinterpret_cast<T *>(intPtr) : nullptr;
}

/*!
\brief Sets the configuration settings file path to \a config_settings_file_path

This initialization function needs to be called by the program that links to
this library.

The static Utils::configSettingsFilePath will then be available throughout the
library code.
*/
void
Utils::initializeConfigSettingsFilePath(
  const QString &config_settings_file_path)
{
  // This static variable now contains the config settings file path
  // dictated by the application that links to this library and initializes
  // the configuration settings file path.
  Utils::configSettingsFilePath = config_settings_file_path;

  // qDebug() << "The Utils::configSettingsFilePath has been set to "
  //          << Utils::configSettingsFilePath;
}

/*!
\brief Returns 0 if both files \a file_path_1 and \a file_path_2 have the same
contents or 1 if theirs contents differ.

Returns -1 if any file could not be opened.
*/
int
Utils::testDiffBetweenTwoTextFiles(const QString &file_path_1,
                                   const QString &file_path_2)
{
  QFile file_1(file_path_1);
  if(!file_1.open(QIODevice::ReadOnly | QIODevice::Text))
    {
      qDebug() << file_path_1 << "could not be opened";
      return -1;
    }

  QFile file_2(file_path_2);
  if(!file_2.open(QIODevice::ReadOnly | QIODevice::Text))
    {
      qDebug() << file_path_2 << "could not be opened";
      return -1;
    }

  QTextStream in_stream_1(&file_1), in_stream_2(&file_2);

  while(!in_stream_1.atEnd() && !in_stream_2.atEnd())
    {
      QString line_1 = in_stream_1.readLine();
      QString line_2 = in_stream_2.readLine();
      if(line_1 != line_2)
        return 1;
    }

  return 0;
}

/*!
\brief Returns a string obtained by concatenating all the strings in the \a
error_list container. Each string is separated from the other with \a separator.
*/
QString
Utils::joinErrorList(const ErrorList &error_list, const QString &separator)
{
  QString text;

  for(const QString &string : error_list)
    text += QString("%1%2").arg(string).arg(separator);

  return text;
}


#ifdef Q_OS_WIN
#include <windows.h>
#include <psapi.h>
#include <tlhelp32.h>

bool
Utils::isProcessRunningInWindows(const QString &process_name)
{
  bool isRunning = false;
  PROCESSENTRY32 entry;
  entry.dwSize = sizeof(PROCESSENTRY32);

  HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

  if(Process32First(snapshot, &entry))
    {
      while(Process32Next(snapshot, &entry))
        {
          if(QString::fromWCharArray(entry.szExeFile) == process_name)
            {
              isRunning = true;
              break;
            }
        }
    }

  CloseHandle(snapshot);
  return isRunning;
}
#endif

#ifdef Q_OS_LINUX
#include <QDir>
#include <QFile>

bool
Utils::isProcessRunningInLinux(const QString &process_name)
{
  QDir procDir("/proc");
  QStringList processes = procDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);

  for(const QString &pid : processes)
    {
      // Check if it's a valid PID (numeric)
      bool ok;
      pid.toInt(&ok);
      if(!ok)
        continue;

      QFile cmdlineFile(QString("/proc/%1/comm").arg(pid));
      if(cmdlineFile.open(QIODevice::ReadOnly))
        {
          QString name =
            QString::fromLocal8Bit(cmdlineFile.readAll()).trimmed();
          if(name == process_name)
            {
              return true;
            }
        }
    }
  return false;
}
#endif

bool
Utils::isProgramRunning(const QString &program_name)
{
  // Try the QProcess method first (most portable)
  QProcess process;

#ifdef Q_OS_WIN
  process.start("tasklist",
                QStringList() << "/NH" << "/FI"
                              << QString("IMAGENAME eq %1").arg(program_name));
#else
  process.start("pgrep", QStringList() << "-x" << program_name);
#endif

  if(process.waitForFinished(1000))
    {
      QString output = process.readAllStandardOutput();

#ifdef Q_OS_WIN
      return output.contains(program_name, Qt::CaseInsensitive);
#else
      return !output.trimmed().isEmpty();
#endif
    }

// Fallback to platform-specific methods if QProcess fails
#ifdef Q_OS_WIN
  return isProcessRunningInWindows(program_name);
#elif defined(Q_OS_LINUX)
  return isProcessRunningInLinux(program_name);
#else
  return false; // macOS or other platforms
#endif
}

void
Utils::registerJsConstructor(QJSEngine *engine)
{
  if(!engine)
    {
      qDebug() << "Cannot register Utils class: engine is null";
      return;
    }

  // Register the meta object as a constructor
  // qDebug() << "Registering JS constructor for class Utils.";

  // The registration below makes it possible to allocate a Utils
  // instance using var the_utils = new Utils()
  // Then the functions are used as the_utils.unspacify("This text has spaces")
  QJSValue jsMetaObject = engine->newQMetaObject(&Utils::staticMetaObject);
  engine->globalObject().setProperty("Utils", jsMetaObject);

  // The registration below creates a singleton exposed as "utils" which
  // makes it possible to use the static functions as
  // utils.unspacify("This text has spaces") without first allocating
  // an instance of the class.
  engine->globalObject().setProperty("utils",
                                     engine->newQObject(new Utils(engine)));
}


} // namespace libXpertMassCore
} // namespace MsXpS
