/* 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 Averagine.hpp

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

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

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

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

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


/*!
\brief Constructs an empty Averagine instance.
*/
Averagine::Averagine(QObject *parent): QObject(parent)
{
  // m_isValid is false, necessarily.
}

/*!
\brief Construct a Averagine instance setting the formula to \a formula.
*/
Averagine::Averagine(const Formula &formula, QObject *parent)
  : QObject(parent), m_formula(formula)
{
  // m_isValid is false, necessarily.
}

/*!
\brief Construct a Averagine instance setting the isotopic data to \a
isotopic_data_csp.
*/
Averagine::Averagine(IsotopicDataCstSPtr isotopic_data_csp, QObject *parent)
  : QObject(parent), mcsp_isotopicData(isotopic_data_csp)
{
  // m_isValid is false, necessarily.
}

/*!
\brief Construct a Averagine instance setting the formula to \a formula
and the isotopic data to \a isotopic_data_csp.
*/
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.";
    }

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

/*!
\brief Construct a Averagine instance as a copy of \a other.
*/
Averagine::Averagine(const Averagine &other, QObject *parent)
  : QObject(parent),
    m_formula(other.m_formula),
    mcsp_isotopicData(other.mcsp_isotopicData)
{
  // qDebug() << "In pseudo copy constructor.";

  ErrorList error_list;

  m_isValid = validate(&error_list);

  if(!m_isValid)
    {
      qCritical() << "Failed to successfully validate the initialized "
                     "Averagine instance.";
    }

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

/*!
\brief Returns a reference to this Averagine after having initialized it using
\a other.
*/
Averagine &
Averagine::initialize(const Averagine &other)
{
  if(this == &other)
    return *this;

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

  ErrorList error_list;

  m_isValid = validate(&error_list);

  if(!m_isValid)
    {
      qCritical() << "Failed to successfully validate the initialized "
                     "Averagine instance.";
    }

  // qDebug() << "Done initializing instance, is valid:" << m_isValid;

  return *this;
}

/*!
\brief Returns a newly allocated Averagine instance with parent set to \a
parent.
*/
Averagine *
Averagine::clone(QObject *parent)
{
  Averagine *copy_p = new Averagine(parent);
  copy_p->initialize(*this);
  return copy_p;
}

/*!
 \ b*rief Returns a newly allocated Averagine instance initialized using \a
 other and with parent set to \a parent.
 */
Averagine *
Averagine::clone(const Averagine &other, QObject *parent)
{
  Averagine *copy_p = new Averagine(parent);
  copy_p->initialize(other);
  return copy_p;
}

/*!
\brief Destructs this Averagine object.

Clear the member coordinateList.
*/
Averagine::~Averagine()
{
}

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

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

const Formula &
Averagine::getFormulaCstRef() const
{
  return m_formula;
}

const Formula &
Averagine::getFormula() const
{
  return getFormulaCstRef();
}

double
Averagine::getMonoMass() const
{
  return m_mono;
}

double
Averagine::getAvgMass() const
{
  return m_avg;
}

void
Averagine::setIsotopicData(const IsotopicDataCstSPtr isotopic_data_csp)
{
  if(mcsp_isotopicData != isotopic_data_csp)
    mcsp_isotopicData = isotopic_data_csp;
}

const IsotopicDataCstSPtr
Averagine::getIsotopicData() const
{
  return mcsp_isotopicData;
}

/*!
\brief Returns true if this instance is identical to \a other, false otherwise.
*/
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.
*/
bool
Averagine::operator!=(const Averagine &other) const
{
  return !operator==(other);
}

double
Averagine::averagineEquivalents(Enums::MassType mass_type,
                                double mass,
                                bool &ok) const
{
  if(!m_isValid)
    {
      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;
}

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 Returns true if this Averagine instance could validate successfully,
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.

The errors, if any, are appended to \p error_list_p, that cannot be nullptr.

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 true if this Averagine instance is valid, false otherwise.
*/
bool
Averagine::isValid() const
{
  return m_isValid;
}

/*!
\brief Returns a string describing this Averagine instance.
*/
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
