/* 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
 */


/////////////////////// Stdlib includes


/////////////////////// Qt includes


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


int ionizerMetaTypeId = qRegisterMetaType<MsXpS::libXpertMassCore::Ionizer>(
  "MsXpS::libXpertMassCore::Ionizer");

int ionizerPtrMetaTypeId =
  qRegisterMetaType<MsXpS::libXpertMassCore::Ionizer *>(
    "MsXpS::libXpertMassCore::IonizerPtr");

namespace MsXpS
{
namespace libXpertMassCore
{


/*!
\class MsXpS::libXpertMassCore::Ionizer
\inmodule libXpertMassCore
\ingroup PolChemDefBuildingdBlocks
\inheaderfile Ionizer.hpp

\brief The Ionizer class provides abstractions to ionize analytes.

Ionizations are chemical reactions that bring a charge (or more
charges) to an analyte. In this Ionizer class, that process is modelled along
with the idea that a previously ionized analyte might be re-ionized with
a different ionization chemistry (that is, either a different ionization agent,
or a different ionization nominal charge or a different ionization level).

To provide the basis for re-ionization, the class provides two ionization
rules describing the ionization state (present and future) of the analyte
managed by the Ionizer:

\list

\li one ionization rule (formula, nominal charge, level) for the "current state"
of ionization;

\li another ionization rule for the "ionization to come", that is the rule that
will be applied to the analyte managed by this Ionizer when the ionize()
function will be called.

\endlist

The class data members that describe the current ionization state of the
analyte managed by the Ionizer have the "currentState" prefix to their name (eg
\e m_currentStateFormula). The class data members that describe the so-called
"ionization-to-come" ionization rule have no prefix associated to their name (eg
\e m_formula).

The ionization chemical reaction is described by the
member \l Formula instances \l Ionizer::m_formula (ionization to come) and \l
Ionizer::m_currentStateFormula (current state of ionization). The electric
charge that is brought by this reaction is described by the members \l
Ionizer::m_nominalCharge and \l
Ionizer::m_currentStateNominalCharge. The
ionization level is described by the members \l Ionizer::m_level and \l
Ionizer::m_currentStateLevel. The ionization level is typically set to the
number of ionization reactions, that is, for example, it would be set to 10 for
a protein to be ionized ten times by protonation, each protonation event
bringing a nominal charge of 1.

An Ionizer like the following, if used to ionize a molecule of Mr 1000

\list
\li Formula +H
\li Charge 1
\li Level 1
\endlist

would lead to an ion of mass 1001 and of m/z 1001.

In protein chemistry, the ionization reaction is mainly a
protonation reaction, which brings a single charge. Thus, the Formula would be
"+H" and the charge 1. In MALDI, we would have a single protonation, thus
level would be set to 1 by default. In electrospray ionization, more than one
ionization reaction occur, and we could have a protein that is 25+, thus
having an ionization level of 25, for example.

An Ionizer like the following, if used to ionize a molecule of Mr 1000

\list
\li Formula +H
\li Charge 1
\li Level 4
\endlist

would lead to an ion of mass 1004 and of m/z 251 (1004 / 4).

An Ionizer like the following, if used to ionize a molecule of Mr 1000

\list
\li Formula +Mg (in fact Mg2+)
\li Charge 2
\li Level 4
\endlist

would lead to an ion of mass 1000 + (24 * 4) and of m/z 137 = (1096 / 8).

\note The real nature of the ionization is beared by the Formula (with, for
example in protein chemistry, "+H" for protonation and, in nucleic acids
chemistry, "-H" for deprotonation).

Thus, an Ionizer is valid if it generates a m/z ratio after ionization of the
analyte that is different than the previous (M) and if its member Formula
validates successfully.  This means that the following should be true:

\list
\li The Formula should be valid (that is, should contain at least
one symbol (which might have a very small mass, like when the ionization is
done by electron gain or loss);

\li The charge is >0 (the ionization event should bring one
charge, otherwise there is no ionization). To reset the ionization
to 0 (that is to deionize the analyte, set the level to 0);

\li The level is >= 0 (if the level is 0, then the analyte is
considered not ionized);
\endlist

\note We do not consider compulsory that the Formula brings
a mass difference whatsoever, because some ionizations might not involve heavy
mass transfers, like electron gain or electron loss. However, the member Formula
must validate successfully and that means that it cannot be empty. In that case,
use a zero-sum formula like (-H+H) that has no weight.

The Ionizer class should be used with caution because its internal state
conditions heavily the reliability of the ionization calculations. The best way
to proceed is to construct the ionizer fully at start and then only use the
functional features of the class (ionize(), deionize(), molecularMasses()).
*/


/*!
\variable int MsXpS::libXpertMassCore::Ionizer::mcsp_isotopicData

\brief The IsotopicData required to actually account for the ionization
formula.

\sa m_currentStateFormula,  m_formula
*/

/*!
\variable int MsXpS::libXpertMassCore::Ionizer::m_formula

\brief The Formula that is used to perform the ionization.

For a protonation event, that would be "+H", for example. For a
deprotonation event, that would be "-H".
*/

/*!
\variable int MsXpS::libXpertMassCore::Ionizer::m_currentStateFormula


\brief The Formula that was last used to ionize the analyte

This formula is only used when an analyte is being ionized. If this formula
and the current state of ionization of the analyte is true, then it is used to
first deionize the analyte.
*/

/*!
\variable int MsXpS::libXpertMassCore::Ionizer::m_nominalCharge

\brief The charge that is brought to the molecule when it is ionized by the
Ionizer with an ionization level (\l m_level) of 1. For a protonation, that
would be \e 1.
*/

/*!
\variable int MsXpS::libXpertMassCore::Ionizer::m_currentStateNominalCharge

\brief The charge that is currently bore by the analyte when it was last
ionized.
*/

/*!
\variable int MsXpS::libXpertMassCore::Ionizer::m_level

\brief The number of times this IonizRule is used to ionize a molecule.

For example, applying this Ionizer to a molecule

\list
\li Formula +H
\li Charge 1
\li Level 4
\endlist

would protonate it 4 times, with a charge of 4 and an increment in mass of the
mass of 4 times 1 proton. The m_level should be construed as "how many times
should the ionization reaction (formula / nominal charge) be performed."
*/

/*!

\variable int MsXpS::libXpertMassCore::Ionizer::m_currentStateLevel

\brief The number of times the "ionization-to-come" formula and nominal charge
will be applied to the analyte managed by this Ionizer upon next call to \l
ionize().
*/

/*!
\variable int MsXpS::libXpertMassCore::Ionizer::m_isValid

\brief Tells if this Ionizer is valid, both for the current state and for the
ionization-to-come member data.

\sa isValid(), validate()
*/

/*!
\variable int MsXpS::libXpertMassCore::Ionizer::m_isCurrentStateValid

\brief Tells if the current ionization state of the analyte managed by this
Ionizer is valid.

\sa isValid(), isCurrentStateValid(), validate(), validateCurrentState()
*/


/*!
\brief  Constructs an Ionizer initialized as an empty object.

The instantiated Ionizer is invalid.
*/
Ionizer::Ionizer(QObject *parent): QObject(parent)
{
}

/*!
\brief  Constructs an Ionizer initialized only with the IsotopicData.

The instantiated Ionizer is invalid.
*/
Ionizer::Ionizer(IsotopicDataCstSPtr isotopic_data_csp, QObject *parent)
  : QObject(parent), mcsp_isotopicData(isotopic_data_csp)
{
}

/*!
\brief  Constructs an Ionizer initialized with \a isotopic_data_csp, \a formula,
\a charge, \a level.

The current state member data are unitialized and the analyte managed by this
Ionizer is thus in an un-ionized state. Instead, if ionize() is called, the
ionization is performed using the data passed to this constructor.

After setting the member data, \l validate() is called and the \l m_isValid
member is set to the result of the validation.

Note that the validation first checks the ionization-to-come members and then
also checks the ionization current state members.

\sa validate(), validateCurrentState()
*/
Ionizer::Ionizer(IsotopicDataCstSPtr isotopic_data_csp,
                 const Formula &formula,
                 int charge,
                 int level,
                 QObject *parent)
  : QObject(parent),
    mcsp_isotopicData(isotopic_data_csp),
    m_formula(formula),
    m_nominalCharge(charge),
    m_level(level)
{
  ErrorList error_list;
  m_isValid = validate(&error_list);

  // qDebug() << "The ionizer formula:" << m_formula.getActionFormula();

  if(!m_isValid)
    qDebug() << "The Ionizer just constructed is invalid, with errors:"
             << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief  Constructs an Ionizer initialized with \a isotopic_data_csp, \a
formula_string, \a charge, \a level.

The current state member data are unitialized and the analyte managed by this
Ionizer is thus in an un-ionized state. Instead, if ionize() is called, the
ionization is performed using the data passed to this constructor.

After setting the member data, \l validate() is called and the \l m_isValid
member is set to the result of the validation.

Note that the validation first checks the ionization-to-come members and then
also checks the ionization current state members.

\sa validate(), validateCurrentState()
*/
Ionizer::Ionizer(IsotopicDataCstSPtr isotopic_data_csp,
                 const QString &formula_string,
                 int charge,
                 int level,
                 QObject *parent)
  : QObject(parent),
    mcsp_isotopicData(isotopic_data_csp),
    m_formula(Formula(formula_string)),
    m_nominalCharge(charge),
    m_level(level)
{
  ErrorList error_list;
  m_isValid = validate(&error_list);

  // qDebug() << "The ionizer formula:" << m_formula.getActionFormula();

  if(!m_isValid)
    qDebug() << "The Ionizer just constructed is invalid, with errors:"
             << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief  Constructs an Ionizer as a copy of \a other.

After setting the member data, \l validate() is called and the \l m_isValid
member is set to the result of the validation.
*/
Ionizer::Ionizer(const Ionizer &other, QObject *parent)
  : QObject(parent),
    mcsp_isotopicData(other.mcsp_isotopicData),
    m_formula(other.m_formula),
    m_nominalCharge(other.m_nominalCharge),
    m_level(other.m_level),
    m_currentStateFormula(other.m_currentStateFormula),
    m_currentStateNominalCharge(other.m_currentStateNominalCharge),
    m_currentStateLevel(other.m_currentStateLevel),
    m_isCurrentStateValid(other.m_isCurrentStateValid)
{
  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug() << "The Ionizer just constructed is invalid, with errors:"
             << Utils::joinErrorList(error_list, ", ");
}

/*!
 \ brief Initializes this Ionizer instance using* \a other.

 After setting the member data, \l validate() is called and the \l m_isValid
 member is set to the result of the validation.
*/
void
Ionizer::initialize(const Ionizer &other)
{
  mcsp_isotopicData           = other.mcsp_isotopicData;
  m_formula                   = other.m_formula;
  m_nominalCharge             = other.m_nominalCharge;
  m_level                     = other.m_level;
  m_currentStateFormula       = other.m_currentStateFormula;
  m_currentStateNominalCharge = other.m_currentStateNominalCharge;
  m_currentStateLevel         = other.m_currentStateLevel;
  m_isCurrentStateValid       = other.m_isCurrentStateValid;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug() << "The Ionizer just constructed is invalid, with errors:"
             << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Sets the IsotopicData to \a isotopic_data_csp.

After setting the member data, \l validate() is called and the \l m_isValid
member is set to the result of the validation.
*/
void
Ionizer::setIsotopicDataCstSPtr(IsotopicDataCstSPtr isotopic_data_csp)
{
  mcsp_isotopicData = isotopic_data_csp;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug() << "The Ionizer is invalid, with errors:"
             << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Returns the IsotopicData.
*/
IsotopicDataCstSPtr
Ionizer::getIsotopicDataCstSPtr() const
{
  return mcsp_isotopicData;
}

/*!
\brief Sets the Formula to \a formula.

The m_formula describes the reaction to be applied to an analyte when the
ionize() function gets called.

After setting the member data, \l validate() is called and the \l m_isValid
member is set to the result of the validation.
*/
void
Ionizer::setFormula(const Formula &formula)
{
  m_formula = formula;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    {
      qDebug() << "The Ionizer did not validate successfully, with errors:"
               << Utils::joinErrorList(error_list, ", ");
    }
}

/*!
\brief Sets the Formula to \a formula_string.

The m_formula describes the reaction to be applied to an analyte when the
ionize() function gets called.

After setting the member data, \l validate() is called and the \l m_isValid
member is set to the result of the validation.
*/
void
Ionizer::setFormula(const QString &formula_string)
{

  m_formula = Formula(formula_string);

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    {
      qDebug() << "The Ionizer did not validate successfully, with errors:"
               << Utils::joinErrorList(error_list, ", ");
    }
}

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

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

/*!
\brief Returns a const reference to the current state Formula.

The m_currentStateFormula describes the formula that has been already applied to
the analyte as it has already been ionized.
*/
const Formula &
Ionizer::getCurrentStateFormulaCstRef() const
{
  return m_currentStateFormula;
}

/*!
\brief Sets the nominal charge to \a nominal_charge.

The m_nominalCharge describes the charge that is brought to the analyte by the
m_formula member when ionize() is called.

After setting the member data, \l validate() is called and the \l m_isValid
member is set to the result of the validation.
*/
void
Ionizer::setNominalCharge(int nominal_charge)
{
  m_nominalCharge = nominal_charge;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    {
      qDebug() << "The Ionizer did not validate successfully, with errors:"
               << Utils::joinErrorList(error_list, ", ");
    }
}

/*!
\brief Returns the nominal charge.
*/
int
Ionizer::getNominalCharge() const
{
  return m_nominalCharge;
}

/*!
\brief Returns the current state nominal charge.

The m_currentStateNominalCharge member datum describes the charge that was
brought by the formula to the analyte when it was ionized (independently from
the ionization level).
*/
int
Ionizer::getCurrentStateNominalCharge() const
{
  return m_currentStateNominalCharge;
}

/*!
\brief Returns the total charge.

The total charge is the product (m_nominalCharge * m_level). This charge will be
brought to the analyte when ionize() gets called.
*/
int
Ionizer::charge() const
{
  return m_nominalCharge * m_level;
}

/*!
\brief Returns the last status total charge.

The total charge is the product (m_currentStateNominalCharge *
m_currentStateLevel). This current state charge is the charge that was brought
to the analyte when ionize() was called previously. It thus reflects the current
charge of the analyte managed by this Ionizer.
*/
int
Ionizer::currentStateCharge() const
{
  return m_currentStateNominalCharge * m_currentStateLevel;
}

/*!
\brief Sets the current state member data.

This is useful when starting from an ionized state (for a Formula, for example)
that will require to be ionized later in a different manner. See, for example,
in MassXpert3, the MzCalculationDlg::getSrcIonizerData() function.

The three parameters thus describe in full the current ionization status of the
analyte managed by this Ionizer:

\list

\li \a formula The formula that describes the current ionization state;

\li \a nominal_charge The charge that is brought by the reaction described in
the formula;

\li \a level The count of ionization events to be applied to the analyte managed
by this Ionizer instance.

\endlist

The m_isCurrentStateValid is set to the result of the validation of the current
state of this Ionizer.
*/
void
Ionizer::forceCurrentState(const Formula &formula,
                           int nominal_charge,
                           int level)
{
  m_currentStateFormula       = formula;
  m_currentStateNominalCharge = nominal_charge;
  m_currentStateLevel         = level;

  ErrorList error_list;
  m_isCurrentStateValid = validateCurrentState(&error_list);
}

/*!
\brief Sets the current state ionization level to \a level.

This function resets the current state ionization level and is practical to use
for setting the analyte managed by this Ionizer to an un-ionized state even if
the other members of the current state are valid (the formula and the nominal
charged are correctly described).
*/
void
Ionizer::forceCurrentStateLevel(int level)
{
  m_currentStateLevel = level;
}

/*!
\brief Sets the ionization level to \a level.

This ionization level describes the number of times the ionization formula and
the nominal charge need to be applied when ionize() gets called on the managed
analyte.

After setting the member data, \l validate() is called and the \l m_isValid
member is set to the result of the validation.
*/
void
Ionizer::setLevel(int level)
{
  m_level = level;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    {
      qDebug() << "The Ionizer did not validate successfully, with errors:"
               << Utils::joinErrorList(error_list, ", ");
    }
}

/*!
\brief Returns the ionization level.

This ionization level describes the number of times the ionization formula and
the nominal charge need to be applied when ionize() gets called on the managed
analyte.
*/
int
Ionizer::getLevel() const
{
  return m_level;
}

/*!
\brief Returns the ionization level.

This ionization level describes the number of times the ionization formula and
the nominal charge have been applied when ionize() was called on the managed
analyte.
*/
int
Ionizer::getCurrentStateLevel() const
{
  return m_currentStateLevel;
}

/*!
\brief

Allocates on the stack an Ionizer instance and initializes its
ionization-to-come data using the current state data from this Ionizer. The
current ionization state of the returned Ionizer instance are reset to nothing,
effectively setting the managed analyte to a non-ionized state.

Returns the allocated Ionizer instance.
*/
Ionizer
Ionizer::makeIonizerWithCurrentStateData()
{
  Ionizer ionizer(mcsp_isotopicData);
  ionizer.m_formula       = m_currentStateFormula;
  ionizer.m_nominalCharge = m_currentStateNominalCharge;
  ionizer.m_level         = m_currentStateLevel;
  ionizer.m_isValid       = m_isCurrentStateValid;

  // All the current state of the new Ionizer are set to un-ionized,
  // which is good.
  ErrorList error_list;

  if(!ionizer.validate(&error_list))
    qFatal() << "The created ionizer is not valid.";

  return ionizer;
}

/*!
\brief Returns the current ionization status.

The analyte managed by this Ionizer is considered ionized if the current state
nominal charge multiplied by the ionization level is not 0.

\sa currentStateCharge()
*/
bool
Ionizer::isIonized() const
{
  return currentStateCharge() ? true : false;
}

/*!
\brief Returns the outcome of the ionization process.

The \a mono and \a avg masses are the masses of the analyte to be ionized. These
masses are passed as non const because they are modified to become m/z values
accounting for the ionization process (or to become Mr values accounting for the
deionization process).

The ionization is performed by accounting into \a mono and \a avg the formula of
the ionization rule compounded by the level of ionization that is required.
Then, the resulting masses are divided by the charge (that is the product of
m_nominalCharge by m_level), thus becoming the m/z value of the ionized analyte.

If this Ionizer instance's m_isIonized member value is true, the function does
not do anything and returns Enums::IonizationOutcome::UNCHANGED.

The modification of \a mono and \a avg is "atomic", that it, it occurs only if
the ionization is successful and leads to values different than those initially
passed to the function.
*/
Enums::IonizationOutcome
Ionizer::ionize(double &mono, double &avg) const
{
  // qDebug() << "Ionizing with this ionizer:" << toString()
  //          << "The current masses are:" << mono << "-" << avg;

  if(!m_isValid)
    {
      qDebug() << "Cannot perform ionization with invalid Ionizer.";
      return Enums::IonizationOutcome::FAILED;
    }

  if(currentStateCharge())
    {
      // qDebug() << "Asking for ionization of an analyte that is already
      // ionized. "
      //            "Performing deionization first.";

      Enums::IonizationOutcome ionization_outcome = deionize(mono, avg);

      if(ionization_outcome == Enums::IonizationOutcome::FAILED)
        qFatal() << "Failed to deionize.";
    }

  if(!charge())
    {
      // qDebug()
      //   << "Asking for ionization of an analyte with ionization charge 0. "
      //      "Performing no action.";

      return Enums::IonizationOutcome::UNCHANGED;
    }

  // qDebug() << qSetRealNumberPrecision(10)
  //          << "Before ionization, Ionizer:" << toString()
  //          << "with entering masses: " << mono << "-" << avg << "\n\n";

  double temp_mono = 0;
  double temp_avg  = 0;

  //  Account for the ionization rule formula in the temp masses.

  // Note the times(m_level) param to call below.
  bool ok = false;

  // To enforce constness of the function
  Formula temp_formula(m_formula);

  temp_formula.accountMasses(
    ok, mcsp_isotopicData, temp_mono, temp_avg, m_level);
  if(!ok)
    {
      qDebug() << "Failed to account the masses of the ionizer formula.";
      return Enums::IonizationOutcome::FAILED;
    }

    /*qDebug() << qSetRealNumberPrecision(10)
             << "The ionization formula:" << temp_formula.getActionFormula()
             << "resolved to computed masses:" << temp_mono << "-" << temp_avg*/
    ;

  // Add the ionization mass to the the masses of the entity to be ionized.

  temp_mono += mono;
  temp_avg  += avg;

  // qDebug() << qSetRealNumberPrecision(10)
  //          << "The ionized analyte has masses:" << temp_mono << "-" <<
  //          temp_avg;

  //  Now account for the charge and thus produce m/z instead of m.

  temp_mono /= charge();
  temp_avg  /= charge();

  // qDebug() << qSetRealNumberPrecision(10)
  //          << "The ionized analyte has m/z:" << temp_mono << "-" << temp_avg;

  //  Now establish if we did actually change something in the process:
  if((temp_mono != mono && temp_avg == avg) ||
     (temp_mono == mono && temp_avg != avg))
    qFatal() << "Programming error. Ionization failed.";

  if(temp_mono == mono && temp_avg == avg)
    qFatal() << "Programming error. Ionization failed.";

  //  Yes, ionization changed something.
  mono = temp_mono;
  avg  = temp_avg;

  m_currentStateFormula       = m_formula;
  m_currentStateNominalCharge = m_nominalCharge;
  m_currentStateLevel         = m_level;
  // m_wasIonized        = true;
  m_isCurrentStateValid       = true;

  // qDebug() << qSetRealNumberPrecision(10)
  //          << "After ionization, "
  //             "exiting masses: "
  //          << mono << "-" << avg << "\n\n";

  return Enums::IonizationOutcome::IONIZED;
}

/*!
\brief Attempts to deionize the analyte and returns the outcome.

The analyte (of which the masses are passed as modifiable \a mono and \a avg
arguments) is deionized using m_formula, m_nominalCharge and m_level.

The following logic is applied:

\list 1

\li If this Ionizable is not ionized, this function does not do anything and
returns Enums::IonizationOutcome::UNCHANGED.

\li If this Ionizer is not valid, this function returns
Enums::IonizationOutcome::FAILED because it makes no sense to try to change the
ionization of an analyte if the Ionizer is invalid.

\li The deionization process is carried out.

\endlist

If the deionization process leads to masses different to \a mono and \a avg,
then it is deemed successful and Enums::IonizationOutcome::DEIONIZED is
returned, otherwise Enums::IonizationOutcome::UNCHANGED is returned.
*/
Enums::IonizationOutcome
Ionizer::deionize(double &mono, double &avg) const
{
  qDebug() << qSetRealNumberPrecision(10)
           << "Asking for deionization with ionizer:" << toString()
           << "and masses:" << mono << "-" << avg << "\n\n";

  if(!m_isValid)
    {
      qDebug() << "Cannot perform ionization with invalid Ionizer.";
      return Enums::IonizationOutcome::FAILED;
    }

  if(!currentStateCharge())
    {
      qDebug() << "Asking for deionization of an analyte that is not ionized. "
                  "Performing no action.";
      return Enums::IonizationOutcome::UNCHANGED;
    }

  // Reverse the electrical effect of ionization using the last ionization
  // state.

  double temp_mono = mono * currentStateCharge();
  double temp_avg  = avg * currentStateCharge();

  qDebug() << qSetRealNumberPrecision(10)
           << "After reversal of the by-the-charge division, new masses:"
           << temp_mono << "-" << temp_avg;

  // Use the last Formula to reverse the chemical effect of ionization.

  // Note the negated 'times'(- m_ionizeRule.level())param to call
  // below so that we revert the chemical action that led level
  // times to the ionization of the analyte. Note that we do not
  // have any compound(level * charge) because we are dealing with
  // matter here, not charges, thus only 'level' is to be taken into
  // consideration.
  bool ok = false;
  // To enforce constness of the function
  Formula temp_formula(m_currentStateFormula);
  temp_formula.accountMasses(
    ok, mcsp_isotopicData, temp_mono, temp_avg, -m_currentStateLevel);
  if(!ok)
    qFatal() << "Failed to account the masses of the ionizer formula.";

  qDebug()
    << qSetRealNumberPrecision(10)
    << "After having removed the ionization last formula masses from the "
       "previously computed (M+z) mono and avg masses: we now get Mr masses:"
    << temp_mono << "-" << temp_avg;

  //  Now establish if we did actually change something in the process:
  if((temp_mono != mono && temp_avg == avg) ||
     (temp_mono == mono && temp_avg != avg) ||
     (temp_mono == mono && temp_avg == avg))
    qFatal() << "The deionization failed.";

  //  Yes, deionization changed something.
  mono = temp_mono;
  avg  = temp_avg;

  m_currentStateFormula.clear();
  m_currentStateNominalCharge = 0;
  m_currentStateLevel         = 0;
  m_isCurrentStateValid       = true;

  return Enums::IonizationOutcome::DEIONIZED;
}

/*!
\brief Sets the molecular mass of the analyte of which the masses are passed as
modifiable parameters in \a mono and \a avg.

The analyte is first deionized (on copy data) and if the deionization process
was successful, \a mono and \a avg are set to the masses of the deionized
analyte (that is, Mr molecular masses).

If the analyte is not ionized, returns Enums::IonizationOutcome::UNCHANGED (and
nothing is changed). If the analyte was actually ionized, then if it is first
successfully deionized, then the analyte is set to deionized and the \a mono
and \a avg masses are set to the Mr molecular mass.
*/
Enums::IonizationOutcome
Ionizer::molecularMasses(double &mono, double &avg) const
{
  qDebug() << "Asking for computation of molecular masses with ionizer"
           << toString() << "and masses" << mono << "-" << avg;

  if(!currentStateCharge())
    {
      qDebug() << "Asking for molecular masses of a non-ionized analyte. "
                  "Leaving them unchanged.";
      return Enums::IonizationOutcome::UNCHANGED;
    }

  double temp_mono = mono;
  double temp_avg  = avg;

  Enums::IonizationOutcome outcome = deionize(temp_mono, temp_avg);

  if(outcome == Enums::IonizationOutcome::FAILED)
    qFatal() << "Programming error. Failed to deionize.";

  if(outcome == Enums::IonizationOutcome::DEIONIZED)
    {
      mono = temp_mono;
      avg  = temp_avg;

      m_currentStateFormula       = m_formula;
      m_currentStateNominalCharge = m_nominalCharge;
      m_currentStateLevel         = 0;
      m_isCurrentStateValid       = true;
    }

  return outcome;
}

/*!
\brief  Assigns \a other to this Ionizer.

After setting the member data, \l validate() is called and the \l m_isValid
member is set to the result of the validation.

Returns a reference to this ionization rule.
*/
Ionizer &
Ionizer::operator=(const Ionizer &other)
{
  if(&other == this)
    return *this;

  mcsp_isotopicData = other.mcsp_isotopicData;
  m_formula         = other.m_formula;
  m_nominalCharge   = other.m_nominalCharge;
  m_level           = other.m_level;
  m_isValid         = other.m_isValid;

  m_currentStateFormula       = other.m_currentStateFormula;
  m_currentStateNominalCharge = other.m_currentStateNominalCharge;
  m_currentStateLevel         = other.m_currentStateLevel;
  m_isCurrentStateValid       = other.m_isCurrentStateValid;

  return *this;
}

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

\note The comparison of the IsotopicData is deep, with a true comparison of all
the Isotope instances, not only of the shared pointers.
*/
bool
Ionizer::operator==(const Ionizer &other) const
{
  if(&other == this)
    return true;

  if(mcsp_isotopicData != nullptr && other.mcsp_isotopicData != nullptr)
    {
      qDebug() << "Now checking the isotopic data";

      if(*mcsp_isotopicData.get() != *other.mcsp_isotopicData.get())
        {
          qDebug() << "The isotopic data are not identical.";
          return false;
        }
    }

  if(m_formula != other.m_formula)
    {
      qDebug() << "The formulas are different.";
      return false;
    }

  if(m_nominalCharge != other.m_nominalCharge)
    {
      qDebug() << "The nominal charges are different.";
      return false;
    }

  if(m_level != other.m_level)
    {
      qDebug() << "The levels are different.";
      return false;
    }

  if(m_isValid != other.m_isValid)
    {
      qDebug() << "The validity status are different.";
      return false;
    }

  if(m_currentStateFormula != other.m_currentStateFormula)
    {
      qDebug() << "The current formulas are different.";
      return false;
    }

  if(m_currentStateNominalCharge != other.m_currentStateNominalCharge)
    {
      qDebug() << "The current nominal charges are different.";
      return false;
    }

  if(m_currentStateLevel != other.m_currentStateLevel)
    {
      qDebug() << "The current levels are different.";
      return false;
    }

  if(m_isCurrentStateValid != other.m_isCurrentStateValid)
    {
      qDebug() << "The current validity statuses are different.";
      return false;
    }

  return true;
}

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

Returns the negated result of operator==().
*/
bool
Ionizer::operator!=(const Ionizer &other) const
{
  if(&other == this)
    return false;

  return !operator==(other);
}

/*!
\brief  Renders the ionization rule XML \a element.

The XML element is parsed and the data extracted from the XML data
are set to this Ionizer instance.

The DTD says this: <!ELEMENT ionizerule(formula,charge,level)>

A typical ionization rule element looks like this:

\code
<ionizerule>
<formula>+H</formula>
<charge>1</charge>
<level>1</level>
</ionizerule>
\endcode

The caller is reponsible for
checking the validity of the IonizeRule prior use.

Returns true if the parsing is successful, false otherwise.

\sa formatXmlIonizeRuleElement(int offset, const QString &indent)
*/
bool
Ionizer::renderXmlIonizeRuleElement(const QDomElement &element)
{
  //  Assume the Ionizer is valid. We'll change that as we go.
  m_isValid = true;

  QDomElement child;

  //   <ionizerule>
  //     <formula>+H</formula>
  //     <charge>1</charge>
  //     <level>1</level>
  //   </ionizerule>

  if(element.tagName() != "ionizerule")
    {
      qDebug() << "Cannot render ionization rule. Problem with the "
                  "<ionizerule> element.";

      m_isValid = false;
      return m_isValid;
    }

  // <formula>
  child = element.firstChildElement();
  if(child.tagName() != "formula")
    {
      qDebug() << "Cannot render ionization rule. Problem with the "
                  "<formula> element.";

      m_isValid = false;
      return m_isValid;
    }

  if(!m_formula.renderXmlFormulaElement(child))
    {
      qDebug() << "Cannot render ionization rule. Problem with the validation"
                  "of the Formula.";

      m_isValid = false;
    }
  qDebug() << "Rendered formula:" << m_formula.getActionFormula();

  // <charge>
  child = child.nextSiblingElement();
  if(child.tagName() != "charge")
    {
      qDebug() << "Cannot render ionization rule. Problem with the "
                  "<charge> element.";

      m_isValid = false;
      return m_isValid;
    }

  bool ok         = false;
  m_nominalCharge = child.text().toInt(&ok);
  if(!m_nominalCharge && !ok)
    {
      qDebug() << "Cannot render ionization rule. Problem with the "
                  "nominal charge value.";

      m_isValid = false;
    }
  qDebug() << "Rendered nominal charge:" << m_nominalCharge;

  // <level>
  child = child.nextSiblingElement();
  if(child.tagName() != "level")
    {
      qDebug() << "Cannot render ionization rule. Problem with the "
                  "<level> element.";

      m_isValid = false;
      return m_isValid;
    }

  ok      = false;
  m_level = child.text().toInt(&ok);
  if(!m_level && !ok)
    {
      qDebug() << "Cannot render ionization rule. Problem with the "
                  "level value.";

      m_isValid = false;
    }
  qDebug() << "Rendered level:" << m_level;

  qDebug() << "At this point validate the Ionizer.";

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug() << "Failed to successfully render <ionizerule> with errors:"
             << Utils::joinErrorList(error_list, ", ");

  return m_isValid;
}

/*!
\brief  Formats and returns a string suitable to use as an XML element.

Formats a string suitable to be used as an XML element in a
polymer chemistry definition file. The typical ionization rule
element that is generated in this function looks like this:

The DTD says this: <!ELEMENT ionizerule(formula,charge,level)>

A typical ionization rule element looks like this:

\code

<ionizerule>
~~<formula>+H</formula>
~~<charge>1</charge>
~~<level>1</level>
</ionizerule>

\endcode

\a offset times the \a indent string must be used as a lead in the
formatting of elements.

\sa renderXmlIonizeRuleElement(const QDomElement &element)
*/
QString
Ionizer::formatXmlIonizeRuleElement(int offset, const QString &indent)
{
  int newOffset;
  int iter = 0;

  QString lead("");
  QString text;


  // Prepare the lead.
  newOffset = offset;
  while(iter < newOffset)
    {
      lead += indent;
      ++iter;
    }

  /* We are willing to create an <ionizerule> node that should look like this:
   *
   *<ionizerule>
   *  <formula>+H</formula>
   *  <charge>1</charge>
   *  <level>1</level>
   *</ionizerule>
   *
   */

  text += QString("%1<ionizerule>\n").arg(lead);

  // Prepare the lead.
  ++newOffset;
  lead.clear();
  iter = 0;
  while(iter < newOffset)
    {
      lead += indent;
      ++iter;
    }

  // Continue with indented elements.

  text += QString("%1<formula>%2</formula>\n")
            .arg(lead)
            .arg(m_formula.getActionFormula(/*with_title*/ true));

  text += QString("%1<charge>%2</charge>\n").arg(lead).arg(m_nominalCharge);

  text += QString("%1<level>%2</level>\n").arg(lead).arg(m_level);

  // Prepare the lead for the closing element.
  --newOffset;
  lead.clear();
  iter = 0;
  while(iter < newOffset)
    {
      lead += indent;
      ++iter;
    }

  text += QString("%1</ionizerule>\n").arg(lead);

  return text;
}

/*!
\brief Validates this Ionizer, setting any error message to \a error_list_p.

An Ionizer is valid if the following data for the ionization-to-come:

\list

\li The member IsotopicData are available

\li The Formula validates successfully (that is, should contain at least
one atom (for a zero-mass formula, use "+H-H", for example);

\li The nominal charge should  be != 0 because otherwise the Ionizer raison
d'être is absent.

\endlist

Further, the current ionization state data are checked and are valid if:

\list

\li The member IsotopicData are available

\li The Formula validates successfully if it is not empty (that is, should
contain at least one atom (for a zero-mass formula, use "+H-H", for example);

\endlist

Note that both the current state nominal charge and level may be 0 (which is
logical if the analyte being managed by this Ionizer is not currently ionized).

If any of the tests above fail, the Ionizer is considered
invalid and the validity status (m_isValid) value is set to false; true
otherwise.

Returns true if validation succeeded, false otherwise.

\sa Formula::validate(), validateCurrentState(), isValid()
*/
bool
Ionizer::validate(ErrorList *error_list_p) const
{
  Q_ASSERT(error_list_p != nullptr);

  qsizetype error_count = error_list_p->size();

  if(mcsp_isotopicData == nullptr || mcsp_isotopicData.get() == nullptr ||
     !mcsp_isotopicData->size())
    {
      qDebug() << "Cannot validate Ionizer without available IsotopicData.";
      error_list_p->push_back(
        "Cannot validate Ionizer without available IsotopicData");
      m_isValid = false;
      return m_isValid;
    }

  // qDebug() << "Going to validate the formula" <<
  // m_formula.getActionFormula();

  if(!m_formula.validate(mcsp_isotopicData, error_list_p))
    {
      QString msg =
        QString("The ionizer formula %1 failed to validate successfully")
          .arg(m_formula.getActionFormula());
      qDebug() << msg;
      error_list_p->push_back(msg);
    }

  // Note that it is perfectly possible that the level of the ionization in
  // the ionizer be 0. Instead, having the nominal charge set to 0 means
  // that the ionizer will never ionize anything even by setting the level
  // to some non-naught value.
  qDebug() << "Going to validate the nominal charge:" << m_nominalCharge;
  if(!m_nominalCharge)
    {
      qDebug()
        << "Cannot validate Ionizer that has its nominal charge equal to 0.";
      error_list_p->push_back(
        "Cannot validate Ionizer that has its nominal charge equal to 0");
    }

  // And now validate the current state

  // qDebug() << "Going to validate the current state data.";
  validateCurrentState(error_list_p);
  // qDebug() << "Done validating the current state data with status:" << ok;

  //  If we added errors, then that means that the Ionizer was not valid.
  m_isValid = (error_list_p->size() > error_count ? false : true);

  if(!m_isValid)
    qDebug() << "Failed to validate this Ionizer, with errors:"
             << Utils::joinErrorList(*error_list_p);

  return m_isValid;
}

/*!
\brief Validates this Ionizer current state, setting any error message to \a
error_list_p.

An Ionizer has a valid current state if:

\list

\li The member IsotopicData are available

\li The Formula validates successfully if it is not empty (that is, should
contain at least one atom (for a zero-mass formula, use "+H-H", for example);

\endlist

Note that both the current state nominal charge and level may be 0 (which is
logical if the analyte being managed by this Ionizer is not currently ionized).

If any of the tests above fail, the Ionizer is considered as
invalid for the current ionization state and the validity status
(m_isCurrentStateValid) value is set to false; true otherwise.

Returns true if validation succeeded, false otherwise.

\sa Formula::validate(), validate(), isValid()
*/
bool
Ionizer::validateCurrentState(ErrorList *error_list_p) const
{
  m_isCurrentStateValid = true;

  if(mcsp_isotopicData == nullptr || mcsp_isotopicData.get() == nullptr ||
     !mcsp_isotopicData->size())
    {
      qDebug() << "Cannot validate Ionizer without available IsotopicData.";

      if(error_list_p != Q_NULLPTR)
        error_list_p->push_back(
          "Cannot validate Ionizer without available IsotopicData");

      m_isCurrentStateValid = false;

      // We really cannot go on with looking into this state.
      return m_isCurrentStateValid;
    }

  // The current state formula might be empty if the analyte state
  // described the the current ionization state is not charged.
  if(!m_currentStateFormula.getActionFormula().isEmpty())
    {
      qDebug() << "Going to validate the current state formula:"
               << m_currentStateFormula.getActionFormula();

      if(!m_currentStateFormula.validate(mcsp_isotopicData, error_list_p))
        {
          QString msg = QString(
                          "The ionizer current state formula %1 failed to "
                          "validate successfully")
                          .arg(m_currentStateFormula.getActionFormula());

          qDebug() << msg;

          if(error_list_p != Q_NULLPTR)
            error_list_p->push_back(msg);

          m_isCurrentStateValid = false;
        }
    }


  // The current state nominal charge and the current state level might be
  // 0 because the user might want to set up the Ionizer in such a way
  // that the analyte is not charged at all.

  if(!m_isCurrentStateValid)
    {
      if(error_list_p != Q_NULLPTR)
        qDebug()
          << "Failed to validate this Ionizer current state, with errors:"
          << Utils::joinErrorList(*error_list_p);
    }
  else
    qDebug() << "The current state formula was validated successfully.";

  return m_isCurrentStateValid;
}

/*!
\brief  Returns the validity status of this instance: true if this Ionizer is
valid, false otherwise.

\sa validateCurrentState(), validate()
*/
bool
Ionizer::isValid() const
{
  return m_isValid;
}

/*!
\brief  Returns the validity status of the current state of this Ionizer
instance: true if the current state is valid, false otherwise.
\sa validate()
*/
bool
Ionizer::isCurrentStateValid() const
{
  return m_isCurrentStateValid;
}

//////////////// UTILS /////////////////////
/*!
\brief Returns a string holding a textual representation of the member data.

If \a with_title is true, the member formulas (m_formula and
m_currentStateFormula) are output with their title (if any).
*/
QString
Ionizer::toString(bool with_title) const
{
  QString text;

  text +=
    QString(
      "Ionizer destination state. formula: %1 - nominal charge: %2 - level: "
      "%3 - is valid: %4\n")
      .arg(m_formula.getActionFormula(with_title))
      .arg(m_nominalCharge)
      .arg(m_level)
      .arg(m_isValid);

  text +=
    QString(
      "Ionizer current state. formula: %1 - nominal charge: %2 - level: %3 "
      "- charge: %4 - was valid: %5\n")
      .arg(m_currentStateFormula.getActionFormula(with_title))
      .arg(m_currentStateNominalCharge)
      .arg(m_currentStateLevel)
      .arg(currentStateCharge())
      .arg(m_isCurrentStateValid);

  return text;
}

/*!
\brief Resets this instance to default values.
*/
void
Ionizer::clear()
{
  mcsp_isotopicData.reset();
  m_formula.clear();
  m_currentStateFormula.clear();
  m_nominalCharge             = 0;
  m_currentStateNominalCharge = 0;
  m_level                     = 0;
  m_currentStateLevel         = 0;
  m_isValid                   = false;
  m_isCurrentStateValid       = false;
}

void
Ionizer::registerJsConstructor(QJSEngine *engine)

{
  if(!engine)
    {
      qDebug() << "Cannot register Ionizer class: engine is null";
      return;
    }

  // Register the meta object as a constructor

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


} // namespace libXpertMassCore
} // namespace MsXpS
