/* BEGIN software license
 *
 * MsXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright 2009--2026 by 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
 */


/////////////////////// Qt includes
#include <QChar>
#include <QDebug>
#include <QString>


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


int averagineMetaTypeId = qRegisterMetaType<MsXpS::libXpertMassCore::Averagine>(
  "MsXpS::libXpertMassCore::Averagine");

namespace MsXpS
{
namespace libXpertMassCore
{


/*!
\class MsXpS::libXpertMassCore::Averagine
\inmodule libXpertMassCore
\ingroup XpertMassMassCoreCalculations
\inheaderfile MsXpS/libXpertMassCore/Averagine.hpp

\brief The Averagine class provides features to use the Averagine concept.

Once the Averagine formula has been set and the isotopic data have been set,
it is possible to compute an elemental composition corresponding to any
arbitrary mass along this path:

\list

\li Compute the Averagine equivalents for the mass at hand
\li Multiply the Averagine's formula indices by the equivalents
\li Return the obtained elemental composition formula with the indices rounded
to integer or kept as double depending on the requirements by the caller.

\endlist
*/

/*!
\variable MsXpS::libXpertMassCore::Averagine::m_formula

\brief Formula of the Averagine. Note that the formula holds decimal
element symbol count indices.
*/

/*!
\property MsXpS::libXpertMassCore::Averagine::formula

\brief Formula of the Averagine. Note that the formula holds decimal
element symbol count indices.
*/

/*!
\variable MsXpS::libXpertMassCore::Averagine::mcsp_isotopicData

\brief The isotopic data to be used for the computations.
*/

/*!
\property MsXpS::libXpertMassCore::Averagine::isotopicData

\brief The isotopic data to be used for the computations.
*/

/*!
\variable MsXpS::libXpertMassCore::Averagine::m_mono

\brief The monoisotopic mass of this Averagine instance.
*/

/*!
\variable MsXpS::libXpertMassCore::Averagine::m_avg

\brief The average mass of this Averagine instance.
*/

/*!
\variable MsXpS::libXpertMassCore::Averagine::m_isValid

\brief Tells if this Averagine instance is valid.
*/

/*!
\brief Constructs a uninitialized Averagine instance.

m_isValid is inherently false.
*/
Averagine::Averagine(QObject *parent): QObject(parent)
{
  // m_isValid is false, necessarily because the m_mono and m_avg masses cannot
  // be computed without the formula nor the isotopic data.
}

/*!
\brief Constructs a Averagine instance setting the formula to \a formula and
parentship to \a parent.

m_isValid is inherently false because the m_mono and m_avg masses cannot
be computed without the isotopic data.
*/
Averagine::Averagine(const Formula &formula, QObject *parent)
  : QObject(parent), m_formula(formula)
{
  // m_isValid is false, necessarily.
}

/*!
\brief Constructs a Averagine instance setting the isotopic data to \a
isotopic_data_csp and parentship to \a parent.

m_isValid is inherently false because the m_mono and m_avg masses cannot
be computed without the formula.
*/
Averagine::Averagine(IsotopicDataCstSPtr isotopic_data_csp, QObject *parent)
  : QObject(parent), mcsp_isotopicData(isotopic_data_csp)
{
  // m_isValid is false, necessarily.
}

/*!
\brief Constructs an Averagine instance setting the formula to \a formula
and the isotopic data to \a isotopic_data_csp. Parentship is established using
\a parent.

m_isValid is set to the value returned by \l validate().

The m_mono and m_avg masses are updated by the call to validate().

\sa validate()
*/
Averagine::Averagine(const Formula &formula,
                     IsotopicDataCstSPtr isotopic_data_csp,
                     QObject *parent)
  : QObject(parent), m_formula(formula), mcsp_isotopicData(isotopic_data_csp)
{
  // qDebug() << "In the constructor.";

  ErrorList error_list;

  m_isValid = validate(&error_list);

  if(!m_isValid)
    {
      qCritical() << "Failed to successfully validate the initialized "
                     "Averagine instance with errors:"
                  << Utils::joinErrorList(error_list);
    }

  // qDebug() << "Done constructing and instance is valid:" << m_isValid;
}

/*!
\brief Constructs a Averagine instance as a copy of \a other. Parentship is
established with \a parent.

After the deep copy, \l validate() is not called.
*/
Averagine::Averagine(const Averagine &other, QObject *parent)
  : QObject(parent),
    m_formula(other.m_formula),
    mcsp_isotopicData(other.mcsp_isotopicData),
    m_mono(other.m_mono),
    m_avg(other.m_avg),
    m_isValid(other.m_isValid)
{
  // qDebug() << "In pseudo copy constructor.";
  // qDebug() << "Done constructing instance with deep copy of the members.
}

/*!
\brief Assigns \a other to this instance, re-establishes parentship to \a parent
and returns a reference to this instance.

After the deep copy, \l validate() is not called.

\sa validate()
*/
Averagine &
Averagine::initialize(const Averagine &other, QObject *parent)
{
  if(this == &other)
    return *this;

  QObject::setParent(parent);

  m_formula         = other.m_formula;
  mcsp_isotopicData = other.mcsp_isotopicData;

  m_mono    = other.m_mono;
  m_avg     = other.m_avg;
  m_isValid = other.m_isValid;

  // qDebug() << "Done initializing instance with deep copy of the members.

  return *this;
}

/*!
\brief Allocates a new Averagine instance with parent set to \a
parent, assigns to it this instance and returns the allocated instance.

The new instance is initialized using this instance.

After the deep copy, \l validate() is not called.

\sa initialize()
*/
Averagine *
Averagine::clone(QObject *parent)
{
  Averagine *copy_p = new Averagine(*this, parent);
  return copy_p;
}

/*!
 \ b*rief Returns a newly allocated Averagine instance initialized with \a
 other and with parent set to \a parent.

The copying is deep, that is, all the members of \a other are copied
and \l validate() is not called.

\sa Averagine::Averagine()
 */
Averagine *
Averagine::clone(const Averagine &other, QObject *parent)
{
  return new Averagine(other, parent);
}

/*!
\brief Destructs this Averagine object.
*/
Averagine::~Averagine()
{
}

/*!
\brief Sets the member Formula to \a formula.
*/
void
Averagine::setFormula(const QString &formula)
{
  if(formula != m_formula.getActionFormula())
    m_formula = Formula(formula);
}

/*!
\brief Sets the member Formula to \a formula.
*/
void
Averagine::setFormula(const Formula &formula)
{
  if(m_formula != formula)
    m_formula = formula;
}

/*!
\brief Returns a const reference to the member formula.
*/
const Formula &
Averagine::getFormulaCstRef() const
{
  return m_formula;
}

/*!
\brief Returns a const reference to the member formula.

This function alias is the getter for the Q_PROPERTY system.
*/
const Formula &
Averagine::getFormula() const
{
  return m_formula;
}

/*!
\brief Returns a reference to the member formula.
*/
Formula &
Averagine::getFormulaRef()
{
  return m_formula;
}

/*!
\brief Returns the monoisotopic mass.
*/
double
Averagine::getMonoMass() const
{
  return m_mono;
}

/*!
\brief Returns the average mass.
*/
double
Averagine::getAvgMass() const
{
  return m_avg;
}

/*!
\brief Sets the isotopic data to \a isotopic_data_csp.
*/
void
Averagine::setIsotopicData(const IsotopicDataCstSPtr isotopic_data_csp)
{
  if(mcsp_isotopicData != isotopic_data_csp)
    mcsp_isotopicData = isotopic_data_csp;
}

/*!
\brief Returns the isotopic data.
*/
const IsotopicDataCstSPtr
Averagine::getIsotopicData() const
{
  return mcsp_isotopicData;
}

/*!
\brief Returns true if this instance is identical to \a other, false otherwise.

The comparison only accounts for the formula and the isotopic data.
*/
bool
Averagine::operator==(const Averagine &other) const
{
  return (m_formula == other.m_formula &&
          mcsp_isotopicData == other.mcsp_isotopicData);
}

/*!
\brief Returns true if this Averagine instance is different than \a other, false
otherwise.

The comparison only accounts for the formula and the isotopic data.
*/
bool
Averagine::operator!=(const Averagine &other) const
{
  return !operator==(other);
}

/*!
\brief Computes the averagine equivalents for \a mass_type \a mass and returns
the result.

In a normal setting, \a mass_type is Enums::MassType::AVG because the idea is to
model a monoisotopic mass on the basis of an average mass.

If an error occurs, \a ok is set to false.

For this function to work, the member m_mono mand m_avg cannot be naught
(qFatal() is triggered).
*/
double
Averagine::averagineEquivalents(Enums::MassType mass_type,
                                double mass,
                                bool &ok) const
{
  if(!m_isValid || !mass)
    {
      ok = false;
      return 0;
    }

  if(mass_type == Enums::MassType::MONO)
    {
      Q_ASSERT(m_mono);

      ok = true;
      return mass / m_mono;
    }

  if(mass_type == Enums::MassType::AVG)
    {
      Q_ASSERT(m_avg);

      ok = true;
      return mass / m_avg;
    }

  ok = false;
  return 0;
}

/*!
\brief Computes the averagine equivalents for \a mass_type \a mass and
determines the corresponding elemental composition formula that it returns.

If \a as_integer_indices is true, the returned elemental composition formula
has integer atom indices.

In a normal setting, \a mass_type is Enums::MassType::AVG because the idea is to
model a monoisotopic mass on the basis of an average mass.

If an error occurs, \a ok is set to false.

Calls averagineEquivalents().

For this function to work, the member m_mono mand m_avg cannot be naught.

\sa averagineEquivalents()
*/
QString
Averagine::elementalComposition(Enums::MassType mass_type,
                                double mass,
                                bool as_integer_indices,
                                bool &ok) const
{
  if(!m_isValid)
    {
      ok = false;
      return QString();
    }

  double averagine_equivalents = averagineEquivalents(mass_type, mass, ok);
  if(!ok || !averagine_equivalents)
    return QString();

  const double avg_c_per_unit = m_formula.symbolCount("C");
  const double avg_h_per_unit = m_formula.symbolCount("H");
  const double avg_n_per_unit = m_formula.symbolCount("N");
  const double avg_o_per_unit = m_formula.symbolCount("O");
  const double avg_s_per_unit = m_formula.symbolCount("S");

  // qDebug() << "Averagine composition (CHNOS)" << avg_c_per_unit
  //          << avg_h_per_unit << avg_n_per_unit << avg_o_per_unit
  //          << avg_s_per_unit;

  // Note how the formula indices are double values
  QString averagine_based_formula(
    QString("C%1H%2N%3O%4S%5")
      .arg(averagine_equivalents * avg_c_per_unit)
      .arg(averagine_equivalents * avg_h_per_unit)
      .arg(averagine_equivalents * avg_n_per_unit)
      .arg(averagine_equivalents * avg_o_per_unit)
      .arg(averagine_equivalents * avg_s_per_unit));

  // Sanity check
  ErrorList error_list;
  // Name it this way because we'll use it later if no error.
  Formula int_based_indices_formula(averagine_based_formula);
  bool is_formula_valid = int_based_indices_formula.validate(
    mcsp_isotopicData, true /*store*/, true /*reset*/, &error_list);

  if(!is_formula_valid)
    {
      qDebug() << "Failed to validate the computed averagine-based elemental "
                  "composition formula.";
      ok = false;
      return QString();
    }

  if(as_integer_indices)
    {
      int_based_indices_formula.roundIndices();

      // Sanity check
      is_formula_valid =
        int_based_indices_formula.validate(mcsp_isotopicData, &error_list);
      if(!is_formula_valid)
        qFatal() << "Failed to validate the int-based averagine-based formula.";

      QString rounded_formula =
        int_based_indices_formula.elementalComposition();
      return rounded_formula;
    }

  return averagine_based_formula;
}

/*!
\brief Computes the averagine equivalents for average mass \a mass and
determines the corresponding elemental composition formula and finally returns
the corresponding monoisotopic mass.

If an error occurs, \a ok is set to false.

Calls elementalComposition().

For this function to work, the member m_mono and m_avg cannot be naught.

\sa elementalComposition()
*/
double
Averagine::modelMonoMass(double mass, bool &ok) const
{
  QString elemental_composition = elementalComposition(
    Enums::MassType::AVG, mass, /*as_integer_indices*/ false, ok);
  if(elemental_composition.isEmpty() || !ok)
    return 0.0;

  Formula formula(elemental_composition);

  double mono;
  double avg;

  formula.accountMasses(ok, mcsp_isotopicData, mono, avg);

  if(!ok)
    qFatal() << "Programming error. Failed to account masses for "
                "averagine-based formula:"
             << formula.getActionFormula() << "from neutral mass:" << mass;

  return mono;
}

/*!
\brief Validates this instance and returns true if validation was successful,
false otherwise.

Validation steps include checking that the isotopic data are correctly set, that
the formula can validate against them and that the masses (mono and avg) of the
averagine can be computed.

\note The m_mono and m_avg values are updated upon validation of the formula.

The errors, if any, are appended to \a error_list_p, that cannot be nullptr
(qFatal()).

m_isValid is set to the result of the validation and is returned.
*/
bool
Averagine::validate(ErrorList *error_list_p)
{
  if(error_list_p == nullptr)
    qFatal() << "Programming error. Pointer cannot be nullptr.";

  qsizetype error_count = error_list_p->size();

  m_isValid = false;

  if(mcsp_isotopicData == nullptr || mcsp_isotopicData.get() == nullptr)
    {
      error_list_p->append(
        "The isotopic data are inexistent. Cannot validate the formula.");
    }
  else
    {
      // We can test for validity of the formula. Eventual errors will
      // be appended to error_list_p.

      if(m_formula.validate(mcsp_isotopicData, error_list_p))
        {
          bool ok = false;
          m_mono  = 0;
          m_avg   = 0;

          m_formula.accountMasses(ok, mcsp_isotopicData, m_mono, m_avg, 1);
          if(!ok)
            error_list_p->append(
              "Failed to compute the masses of the Averagine.");
        }
    }

  m_isValid = (error_list_p->size() == error_count ? true : false);

  qDebug() << "Returning m_isValid:" << m_isValid;

  return m_isValid;
}

/*!
\brief Returns m_isValid: true if this Averagine instance is valid, false
otherwise.
*/
bool
Averagine::isValid() const
{
  return m_isValid;
}

/*!
\brief Crafts a string documenting this instance and returns it.
*/
QString
Averagine::toString() const
{
  // qDebug() << "\n~~~~~~~~~~~~~~ Averagine instance ~~~~~~~~~~~~~\n"
  //          << this << "\n"
  //          << "m_deepCalculation:" << m_deepCalculation
  //          << "\n m_massType:" << massTypeMap[m_massType]
  //          << "\n m_capType:" << capTypeMap[m_capType] << "\n ";

  return QString("m_formula: %1\n")
    .arg(m_formula.getActionFormula(true /*with_title*/));
}

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

  // Register the meta object as a constructor

  QJSValue jsMetaObject = engine->newQMetaObject(&Averagine::staticMetaObject);
  engine->globalObject().setProperty("Averagine", jsMetaObject);
}


} // namespace libXpertMassCore
} // namespace MsXpS
