/* 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
#include <QString>
#include <QDebug>


/////////////////////// Local includes
#include "MsXpS/libXpertMassCore/globals.hpp"
#include "MsXpS/libXpertMassCore/Formula.hpp"
#include "MsXpS/libXpertMassCore/Utils.hpp"
#include "MsXpS/libXpertMassCore/IsotopicData.hpp"
#include "MsXpS/libXpertMassCore/Modif.hpp"
#include "MsXpS/libXpertMassCore/Monomer.hpp"


int modifMetaTypeId = qRegisterMetaType<MsXpS::libXpertMassCore::Modif>(
  "MsXpS::libXpertMassCore::Modif");

int modifPtrMetaTypeId = qRegisterMetaType<MsXpS::libXpertMassCore::Modif *>(
  "MsXpS::libXpertMassCore::ModifPtr");

namespace MsXpS
{
namespace libXpertMassCore
{


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

\brief The Modif class provides abstractions to work with chemical
modifications.

The Modif class provides a chemical modification that can be set to any monomer
in a polymer sequence or to any one of the polymer sequence ends. In the protein
world, chemical modifications of proteins that occur in the living cell are
called post-translational modifications.This class aims at modelling, among
others, such modifications.

The chemical reaction described by the Modif class is encoded as an
action-formula (see \l{Formula}).
*/

/*!
\variable MsXpS::libXpertMassCore::Modif::mcsp_polChemDef

\brief The \l PolChemDef polymer chemistry definition.

This member allows to root the Modif instance in a chemical context where
IsotopicData can be used to calculate masses starting from a Formula, but also
where the Monomer instances can be checked for existence when validating
the Modif targets, for example.
*/

/*!
\variable MsXpS::libXpertMassCore::Modif::m_name

\brief Name of the chemical modification,  like "Acetylation".
*/

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

\brief \l Formula string representing the chemical modification.
*/

/*!
\variable MsXpS::libXpertMassCore::Modif::m_targets

\brief String that holds a list of all the target monomers of this
modification.

If there are more than one target, the targets (Monomer codes)
must be separated by ';' characters.

If any monomer in the polymer chemistry definition might be modified by this
Modif object, then, the "*" string can be used to indicate so.
*/

/*!
\variable MsXpS::libXpertMassCore::Modif::m_maxCount

\brief Value indicating the maximum number of times this modification
can be set to a target entity (monomer or polymer).

The value cannot be less than one, otherwise the Modif is set to an invalid
state.

This member is designed to prohibit modifying more than chemically possible a
given Monomer. For example it is not possible to phosphorylate a Seryl residue
more than once.
*/

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

\brief Value representing the monoisotopic mass of the formula.
*/

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

\brief Value representing the average mass of the formula.
*/

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

\brief Validity status of the Modif instance.
*/

/*!
\brief Constructs a modification starting from an XML <mdf> \a element according
to \a version and using the reference polymer chemistry definition \a
pol_chem_def_csp.

The \a version indicates what version of the XML element is to be used.

This is the current format:
\code
<mdf>
<name>Acetylation</name>
<formula>C2H2O1</formula>
<targets>;K;</targets>
<maxcount>1</maxcount>
</mdf>
<mdf>
<name>AmidationAsp</name>
<formula>H1N1-O1</formula>
<targets>;D;</targets>
<maxcount>1</maxcount>
</mdf>
\endcode

The XML element is rendered using the dedicated function renderXmlMdfElement().

\sa renderXmlMdfElement()
*/
Modif::Modif(PolChemDefCstSPtr pol_chem_def_csp,
             const QDomElement &element,
             [[maybe_unused]] int version)
  : mcsp_polChemDef(pol_chem_def_csp)
{
  if(mcsp_polChemDef == nullptr || mcsp_polChemDef.get() == nullptr)
    qDebug() << "Constructing Modif with no PolChemDef.";

  if(!renderXmlMdfElement(element, version))
    {
      qDebug() << "Failed to fully render or validate the Modif XML element "
                  "for construction of Modif instance.";
    }
}

/*!
\brief Constructs a modification.

A Modif instance cannot be of any use if it is not associated logically to a
polymer chemistry definition (\a pol_chem_def_csp). The Modif instance is
defined by its \a modif_name and its \a formula_string as as string (that
defaults to empty in this constructor).

Being able to construct a Modif without a formula string is necessary when Modif
objects are intialized piecemeal upon reading XML elements that describe the
Modif.

The \a formula_string might be a simple formula ("O", for an oxidation) or an
action-formula, if that is desirable to best characterize the modification
("-H20+CH3COOH", for example, for an acetylation). The formula string can also
have a title, like:

\c{"Acetylation"-H20+CH3COOH}.

The member string datum representing the allowed targets of this Modif
instanceis set to "all" (that is, \c{"*"}) and the maximum count that this
modification can bet set to a given target is set to \c{1} by default, like a
Seryl residue can only be phosphorylated once, for example.

After setting the member data and if the polymer chemistry definition is
available, \l validate() is called,  the masses are calculated and if all went
without error the member \l m_isValid validity status is set to true, otherwise
it is set to false.
*/
Modif::Modif(PolChemDefCstSPtr pol_chem_def_csp,
             const QString &modif_name,
             const QString &formula_string,
             double mono,
             double avg)
  : mcsp_polChemDef(pol_chem_def_csp),
    m_name(modif_name),
    m_formula(formula_string),
    m_mono(mono),
    m_avg(avg)
{
  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    {
      qDebug() << "Construction of Modif with validation errors:\n"
               << Utils::joinErrorList(error_list, ", ");
    }

  // Validation tries to compute masses but does not update member data.
  // Here we want to update the member data.
  if(!calculateMasses(nullptr))
    {
      m_isValid = false;
      qDebug() << "Construction of Modif with masses that failed to be "
                  "calculated.";
    }
}

/*!
\brief Constructs a Modif object as a copy of \a other.

After setting the member data and if the polymer chemistry definition is
available, \l validate() is called,  the masses are calculated and if all went
without error the member \l m_isValid validity status is set to true, otherwise
it is set to false.
*/
Modif::Modif(const Modif &other)
  : PropListHolder(other),
    mcsp_polChemDef(other.mcsp_polChemDef),
    m_name(other.m_name),
    m_formula(other.m_formula),
    m_targets(other.m_targets),
    m_maxCount(other.m_maxCount),
    m_mono(other.m_mono),
    m_avg(other.m_avg)
{
  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    {
      qDebug() << "Construction of Modif with validation errors:\n"
               << Utils::joinErrorList(error_list, ", ");
    }

  // Validation tries to compute masses but does not update member data.
  // Here we want to update the member data.
  if(!calculateMasses(nullptr))
    {
      m_isValid = false;
      qDebug() << "Construction of Modif with masses that failed to be "
                  "calculated.";
    }
}

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

/*!
\brief Sets the polymer chemistry definition to \a pol_chem_def_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
Modif::setPolChemDefCstSPtr(PolChemDefCstSPtr pol_chem_def_csp)
{
  mcsp_polChemDef = pol_chem_def_csp;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug() << "Failed to validate the Modif with errors:\n"
             << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Returns the polymer chemistry definition.
*/
const PolChemDefCstSPtr &
Modif::getPolChemDefCstSPtr() const
{
  return mcsp_polChemDef;
}

//////////////// THE NAME /////////////////////

/*!
\brief Sets the \a name.

After setting the member data, \l validate() is called and the \l m_isValid
member is set to the result of the validation.
*/
void
Modif::setName(const QString &name)
{
  m_name = name;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug() << "Failed to validate the Modif with errors:\n"
             << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Returns the name.
*/
QString
Modif::getName() const
{
  return m_name;
}

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

After setting the member data and if the polymer chemistry definition is
available, \l validate() is called,  the masses are calculated and if all went
without error the member \l m_isValid validity status is set to true, otherwise
it is set to false.
*/
void
Modif::setFormula(const QString &formula_string)
{
  m_formula = formula_string;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    {
      qDebug() << "Failed to validate the Modif with errors:\n"
               << Utils::joinErrorList(error_list, ", ");
    }

  // Validation tries to compute masses but does not update member data.
  // Here we want to update the member data.
  if(!calculateMasses(nullptr))
    {
      m_isValid = false;

      qDebug()
        << "Failed to calculate the masses after setting formula to Modif.";
    }
}

/*
\brief Returns the formula describing this modification.
*/
const QString &
Modif::getFormula() const
{
  return m_formula;
}

/*
\brief Returns a copy of the member formula describing this modification.

If \a with_title is true, the title of the formula prepended to the formula
string.

For example, with title, formula would be \c{"Acetylation"-H2O+CH3COOH}.
*/
QString
Modif::formula(bool with_title) const
{
  Formula temp_formula(m_formula);

  return temp_formula.getActionFormula(with_title);
}

/*!
\brief Sets the \a targets for this modification.

Setting the targets of a Modif instance means specifying which target (Monomer
code) might be modified using this modification. For example, for \c
Phosphorylation, in protein chemistry, one would define targets as \c Serine, \c
Threonine, \c Tyrosine (there are other targets, like Histidine, but very rarely
encountered).

Multiple targets are separated using ';'.

The \a targets are validated (\l Modif::validateTargets()) and the obtained
string is set to m_targets. If the validation fails,  m_targets is set to
QString().

After setting the member data, \l validate() is called and the \l m_isValid
member is set to the result of the validation.
*/
void
Modif::setTargets(const QString &targets)
{
  qDebug() << "Validating targets:" << targets;
  QString local_targets_string = targets;
  local_targets_string         = Utils::unspacify(local_targets_string);

  qDebug() << "After unspacification" << local_targets_string;

  bool ok = false;

  // Validate without simplification (false).
  local_targets_string =
    validateTargets(local_targets_string, /* simplification */ false, ok);

  if(!ok)
    {
      qDebug() << "The validation of the Modif targets failed:"
               << local_targets_string;
      m_targets = QString();
    }
  else
    m_targets = local_targets_string;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug() << "Failed to validate the Modif with errors:"
             << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Returns the tagets of this modification.
*/
QString
Modif::getTargets() const
{
  return m_targets;
}

/*!
\brief Returns true if Mnomer \a code is found among the targets of this
Modif instance, false otherwise.
*/
bool
Modif::doesTargetMonomer(const QString &code) const
{
  if(m_targets == "*")
    return true;

  if(m_targets == "!")
    return false;

  QString delimitedCode = QString(";%1;").arg(code);

  // The m_targets string is in the form ";code;code;code;".

  // qDebug() << "The targets:" << m_targets;

  return m_targets.contains(delimitedCode, Qt::CaseSensitive);
}

/*!
\brief Sets the maximum count (times the modification event is allowed to occur
on a target) that this modification might be set to a target to \a value.

For Phosphorylation, for example, that would be \c{1} count exclusively for
modification of Seryl, Threonyl and Tyrosinyl residues.

After setting the member data, \l validate() is called and the \l m_isValid
member is set to the result of the validation.
*/
void
Modif::setMaxCount(int value)
{
  m_maxCount = value;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug() << "Failed to validate the Modif with errors:"
             << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Returns the maximum count of times that this modification might be
set to a target.
*/
int
Modif::getMaxCount() const
{
  return m_maxCount;
}

//////////////// OPERATORS /////////////////////
/*!
\brief Assigns \a other to this modification.

Returns a reference to this modification.

After setting the member data, \l validate() is called and the \l m_isValid
member is set to the result of the validation.
*/
Modif &
Modif::operator=(const Modif &other)
{
  if(&other == this)
    return *this;

  mcsp_polChemDef = other.mcsp_polChemDef;
  m_name          = other.m_name;
  m_formula       = other.m_formula;
  m_targets       = other.m_targets;
  m_maxCount      = other.m_maxCount;
  m_mono          = other.m_mono;
  m_avg           = other.m_avg;

  PropListHolder::operator=(other);

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    {
      qDebug() << "Assignment of Modif with validation errors:\n"
               << Utils::joinErrorList(error_list, ", ");
    }

  return *this;
}

/*!
\brief Returns true if this and the \a other modifications are identical,
false otherwise.
*/
bool
Modif::operator==(const Modif &other) const
{
  if(&other == this)
    return true;

  //  We cannot compare the PolChemDef,  because that would cause
  //  an infinite loop: (each instance of this class in the PolChemDef would
  //  try to compare the PolChemDef...).

  return m_name == other.m_name && m_formula == other.m_formula &&
         m_targets == other.m_targets && m_maxCount == other.m_maxCount &&
         m_mono == other.m_mono && m_avg == other.m_avg;
}

/*!
\brief Returns true if this and the \a other modifications differ,
false otherwise.

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

  return !operator==(other);
}

//////////////// VALIDATIONS /////////////////////
/*!
\brief Returns the Modif instance from the polymer chemistry definition
registered in this instance.

The key to search the Modif is this instance's member name.

If there is no PolChemDef available, nullptr is returned.

If no Modif instance is found by this instance's name, nullptr is returned.
*/
ModifCstSPtr
Modif::getFromPolChemDefByName() const
{
  if(mcsp_polChemDef == nullptr || mcsp_polChemDef.get() == nullptr)
    return nullptr;

  if(m_name.isEmpty())
    return nullptr;

  return mcsp_polChemDef->getModifCstSPtrByName(m_name);
}

/*!
\brief Returns the status of this Modif instance the polymer chemistry
definition registered in this instance.

The key to search the Modif is this instance's member name.

If there is no PolChemDef available,
Enums::PolChemDefEntityStatus::POL_CHEM_DEF_NOT_AVAILABLE is returned.

If no Modif instance is found by this instance's name,
Enums::PolChemDefEntityStatus::ENTITY_NOT_KNOWN is returned,  otherwise
Enums::PolChemDefEntityStatus::ENTITY_KNOWN is returned.
*/
Enums::PolChemDefEntityStatus
Modif::isKnownByNameInPolChemDef() const
{
  if(mcsp_polChemDef == nullptr || mcsp_polChemDef.get() == nullptr)
    return Enums::PolChemDefEntityStatus::POL_CHEM_DEF_NOT_AVAILABLE;

  if(mcsp_polChemDef->getModifCstSPtrByName(m_name) != nullptr)
    return Enums::PolChemDefEntityStatus::ENTITY_KNOWN;

  return Enums::PolChemDefEntityStatus::ENTITY_NOT_KNOWN;
};

/*!
\brief Validates the \a targets_string target and returns the processed
targets string.

If \a targets_string is non-empty,  it is validated. Otherwise, \a ok is set
to false and \a targets_string is returned unchanged.

If \a simplify is true,  then,  whenever '*' or '!' is encountered in \a
targets_string,  then \a ok is set to true and either '*' or '!' is returned.
Indeed,  the target list is split using ';' as a delimiter. If '*' is found
and \a simplify is true, the function returns '*' immediately (as all the
monomers in the polymer chemistry definition might be a target of this
modification) and \a ok is set to true. Likewise,  if '!' is found and \a
simplify is true, the function returns '!' immediately (as none of all the
monomers in the polymer chemistry definition might be a target of this
modification) and \a ok is set to true.

If at least one target is found, then each target is a monomer
code and that code is looked for in the member polymer chemistry definition
list of monomers. If the code is found, that code is added to a temporary
string. If the code is not found, \a ok is set to false and \a targets_string
is returned unchanged.

When the process completes, without error,  \a ok is set to true and the
string corresponding to the processed monomer codes in the \a targets_string
is returned. Otherwise,  \a ok is set to false and an empty string is
returned.
*/
QString
Modif::validateTargets(const QString &targets_string,
                       bool simplify,
                       bool &ok) const
{
  if(mcsp_polChemDef == nullptr || mcsp_polChemDef.get() == nullptr)
    {
      qDebug()
        << "Cannot validate the targets because no PolChemDef is defined.";
      m_isValid = false;
      return QString();
    }

  QString local_targets_string = targets_string;

  // If the local_targets_string is empty, this is an error, because we cannot
  // know what's to be done with the modification.
  if(local_targets_string.isEmpty())
    {
      ok = false;
      return QString();
    }

  // A targets string cannot contain both a '*' and a '!'
  // character. We check that immediately.
  if(local_targets_string.contains('*') && local_targets_string.contains('!'))
    {
      qDebug()
        << "A modification targets string cannot contain both '*' and '!'.";

      ok = false;
      return QString();
    }

  QString processed_targets_string;

  // A targets string looks like "Ser ; Thr ; Tyr".
  QStringList code_string_list =
    local_targets_string.split(';', Qt::SkipEmptyParts, Qt::CaseSensitive);

  for(int iter = 0; iter < code_string_list.size(); ++iter)
    {
      QString iter_code_string = code_string_list.at(iter);

      // There are two character that might be encountered: '*' is the
      // equivalent of "all the monomers in the definition"; '!' is
      // equivalent to "none of the monomers in the definition". But
      // it is not possible that both * and ! be present in the same
      // targets string.

      if(iter_code_string == "*" || iter_code_string == "!")
        {
          // Simplification asked: if '*' is found then it can be
          // there alone. Same for '!'. '*' means that any monomer in
          // the definition might be modified with this modification,
          // '!' means that none of the monomers might be modified.

          if(simplify)
            {
              // qDebug() << "Simplification asked for.";
              processed_targets_string = iter_code_string;

              break;
            }
          else
            {
              processed_targets_string.append(
                QString("%1;").arg(iter_code_string));
            }

          continue;
        }

      // At this point, we have something to check as a monomer code:

      if(mcsp_polChemDef->getMonomerCstSPtrByCode(iter_code_string) != nullptr)
        {
          // Want the string to be ;code;code;code;(notice the first
          // and last ';'), so that we can later ask easily if ;Lys;
          // is found in the targets string, without risking to also
          // match ;Ly;.
          if(!processed_targets_string.size())
            processed_targets_string.append(
              QString(";%1;").arg(iter_code_string));
          else
            processed_targets_string.append(
              QString("%1;").arg(iter_code_string));
        }
      else
        {
          qDebug() << "Monomer code is not known:" << iter_code_string;
          ok = false;
          return QString();
          ;
        }
    }
  //  End of
  // for(int iter = 0; iter < code_string_list.size(); ++iter)

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

  if(processed_targets_string.isEmpty())
    {
      qDebug()
        << "After the validation process, the targets string results empty.";
      ok = false;
      return QString();
      ;
    }

  //  Most evident fix to the loop output above
  if(processed_targets_string == "*;")
    processed_targets_string = "*";
  if(processed_targets_string == "!;")
    processed_targets_string = "!";

  ok = true;
  return processed_targets_string;
}

/*!
\brief Validates the member targets and returns the processed
targets string.

If \a simplify is true,  then,  whenever '*' or '!' is encountered in the member
targets string,  then \a ok is set to true and either '*' or '!' is returned.
Indeed,  the target list is split using ';' as a delimiter. If '*' is found
and \a simplify is true, the function returns '*' immediately (as all the
monomers in the polymer chemistry definition might be a target of this
modification) and \a ok is set to true. Likewise,  if '!' is found and \a
simplify is true, the function returns '!' immediately (as none of all the
monomers in the polymer chemistry definition might be a target of this
modification) and \a ok is set to true.

If at least one target is found, then each target is a monomer
code and that code is looked for in the member polymer chemistry definition
list of monomers. If the code is found, that code is added to a temporary
string. If the code is not found, \a ok is set to false and the member targets
string is returned unchanged.

When the process completes, without error,  \a ok is set to true and the
string corresponding to the processed monomer codes in the member targets string
is returned. Otherwise,  \a ok is set to false and an empty string is
returned.
*/
QString
Modif::validateTargets(bool simplify, bool &ok)
{
  return validateTargets(m_targets, simplify, ok);
}

/*!
\brief Validates this modification, sets the validity status accordingly and
returns it.

Any potential error is reported with a message added to \a error_list_p (that is
not cleared).

The modification validates successfully if:

\list
\li The member polymer chemistry definition is available and the isotopic data
inside it also

\li The name is not empty

\li The formula validates successfully

\li The masses can be calculated (on local mass variables, given constness of
this function)

\li The targets validate successfully (that is,  they reference Monomer codes
known to the polymer chemistry definition)

\li The m_maxCount member is greater than 0
\endlist

If the validation is successful, the validity status (m_isValide) is set to
true,  to false otherwise,  and returned.
*/
bool
Modif::validate(ErrorList *error_list_p) const
{
  Q_ASSERT(error_list_p != nullptr);

  qsizetype error_count = error_list_p->size();

  m_isValid = false;

  if(mcsp_polChemDef == nullptr || mcsp_polChemDef.get() == nullptr ||
     mcsp_polChemDef->getIsotopicDataCstSPtr() == nullptr ||
     mcsp_polChemDef->getIsotopicDataCstSPtr().get() == nullptr ||
     !mcsp_polChemDef->getIsotopicDataCstSPtr()->size())
    {
      error_list_p->push_back(
        "The PolChemDef or the IsotopicData are not available");
      qDebug()
        << "The polymer chemistry definition member datum is nullptr or "
           "its isotopic data are be either nullptr or empty. Modif validation "
           "failed.";

      return false;
    }

  if(m_name.isEmpty())
    {
      qDebug() << "The Modif name is empty.";
      error_list_p->push_back("The Modif name is empty");
    }

  Formula temp_formula(m_formula);
  if(!temp_formula.validate(mcsp_polChemDef->getIsotopicDataCstSPtr(),
                            error_list_p))
    {
      qDebug() << "The Modif formula failed to validate.";
      error_list_p->push_back("The Modif formula failed to validate");
    }

  double mono = 0.0;
  double avg  = 0.0;

  if(!calculateMasses(mcsp_polChemDef->getIsotopicDataCstSPtr(), mono, avg))
    {
      qDebug() << "Failed to calculate the Modif masses.";
      error_list_p->push_back("Failed to calculate the Modif masses");
    }

  bool ok = false;

  // Validate without simplification (false).
  QString targets_after_validation =
    validateTargets(m_targets, /* simplification */ false, ok);

  if(!ok)
    {
      qDebug() << "The Modif targets failed to validate.";
      error_list_p->push_back("The Modif targets failed to validate");
    }

  if(m_maxCount <= 0)
    {
      qDebug() << "The maximum modification count failed to validate.";
      error_list_p->push_back(
        "The maximum modification count failed to validate");
    }

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

  return m_isValid;
}

/*!
\brief Returns the validity status of this Modif instance.
*/
bool
Modif::isValid() const
{
  return m_isValid;
}

//////////////// MASS CALCULATIONS /////////////////////
/*!
\brief Calculates the net masses of this modification and sets the results in
\a mono and \a avg.

The masses of the modification are the masses (monoisotopic and average)
that are added to the target as a result of that target being modified with
this modification.

The mass calculations are performed using reference data in \a
isotopic_data_csp. If \a isotopic_data_csp is nullptr,  then the reference data
are searched in the member polymer chemistry definition.

Returns true if the mass calculations were successful, false otherwise.

If the calculations failed,  m_isValid is set to false.

\sa Formula::accountMasses()
*/
bool
Modif::calculateMasses(const IsotopicDataCstSPtr &isotopic_data_csp,
                       double &mono,
                       double &avg) const
{
  IsotopicDataCstSPtr local_isotopic_data_csp = isotopic_data_csp;

  if(local_isotopic_data_csp == nullptr ||
     local_isotopic_data_csp.get() == nullptr)
    {
      if(mcsp_polChemDef != nullptr)
        local_isotopic_data_csp = mcsp_polChemDef->getIsotopicDataCstSPtr();

      if(local_isotopic_data_csp == nullptr ||
         local_isotopic_data_csp.get() == nullptr)
        {
          qDebug() << "Failed to find usable isotopic data.";
          m_isValid = false;
          return false;
        }
    }

  // qDebug() << "Calculating masses for" << m_name;

  mono = 0;
  avg  = 0;

  bool ok;

  // Formula temp_formula(m_formula);
  Formula(m_formula).accountMasses(ok, local_isotopic_data_csp, mono, avg);

  if(!ok)
    {
      qDebug() << "Failed accounting masses for Modif:" << m_name
               << "and formula:" << m_formula;
      m_isValid = false;
    }

  return ok;
}

/*!
\brief Calculates the net masses of this modification.

The masses of the modification are the masses (monoisotopic and average)
that are added to the target as a result of that target being modified with
this modification.

The calculated masses are set to the m_mono and m_avg members.

The mass calculations are performed using reference data in \a
isotopic_data_csp. If \a isotopic_data_csp is nullptr,  then the reference data
are searched in the member polymer chemistry definition.

Returns true if the mass calculations were successful, false otherwise.

If the calculations failed,  m_isValid is set to false.

\sa Formula::accountMasses()
*/
bool
Modif::calculateMasses(const IsotopicDataCstSPtr &isotopic_data_csp)
{
  IsotopicDataCstSPtr local_isotopic_data_csp = isotopic_data_csp;

  if(local_isotopic_data_csp == nullptr ||
     local_isotopic_data_csp.get() == nullptr)
    {
      if(mcsp_polChemDef != nullptr)
        local_isotopic_data_csp = mcsp_polChemDef->getIsotopicDataCstSPtr();

      if(local_isotopic_data_csp == nullptr ||
         local_isotopic_data_csp.get() == nullptr)
        {
          qDebug() << "Failed to find usable isotopic data.";
          return false;
        }
    }

  // qDebug() << "Calculating masses for" << m_name;

  m_mono = 0;
  m_avg  = 0;

  bool ok;

  // Formula temp_formula(m_formula);
  Formula(m_formula).accountMasses(ok, local_isotopic_data_csp, m_mono, m_avg);

  if(!ok)
    {
      qDebug() << "Failed accounting masses for Modif:" << m_name
               << "and formula:" << m_formula;
      m_isValid = false;
    }

  return ok;
}

/*!
\brief Calculates the net masses of this modification.

The masses of the modification are the masses (monoisotopic and average)
that are added to the target as a result of that target being modified with
this modification.

The calculated masses are set to the m_mono and m_avg members.

The mass calculations are performed using reference data in \a
isotopic_data_csp. If \a isotopic_data_csp is nullptr,  then the reference data
are searched in the member polymer chemistry definition.

Returns a reference to this Modif instance.

If the calculations failed, m_isValid is set to false.

\sa Formula::accountMasses()
*/
Modif &
Modif::calculateMasses(bool &ok, const IsotopicDataCstSPtr &isotopic_data_csp)
{
  IsotopicDataCstSPtr local_isotopic_data_csp = isotopic_data_csp;

  if(local_isotopic_data_csp == nullptr ||
     local_isotopic_data_csp.get() == nullptr)
    {
      if(mcsp_polChemDef != nullptr)
        local_isotopic_data_csp = mcsp_polChemDef->getIsotopicDataCstSPtr();

      if(local_isotopic_data_csp == nullptr ||
         local_isotopic_data_csp.get() == nullptr)
        {
          qDebug() << "Failed to find usable isotopic data.";
          ok = false;
          return *this;
        }
    }

  // qDebug() << "Calculating masses for" << m_name;

  m_mono = 0;
  m_avg  = 0;

  // Formula temp_formula(m_formula);
  Formula(m_formula).accountMasses(ok, local_isotopic_data_csp, m_mono, m_avg);

  if(!ok)
    {
      qDebug() << "Failed accounting masses for Modif:" << m_name
               << "and formula:" << m_formula;
      m_isValid = false;
    }

  return *this;
}

/*!
\brief Adds to \a mono_p and \a avg_p the corresponding mass of this
modification.

The m_mono and m_avg masses are added to the arguments. The masses are
compounded by factor \a times before the addition.

Returns this object.
*/
const Modif &
Modif::accountMasses(double *mono_p, double *avg_p, int times) const
{
  if(mono_p != nullptr)
    *mono_p += m_mono * times;

  if(avg_p != nullptr)
    *avg_p += m_avg * times;

  return *this;
}

/*!
\brief Adds to \a mono and \a avg the corresponding mass of this
modification.

The m_mono and m_avg masses are added to the arguments. The masses are
compounded by factor \a times before the addition.

Returns this object.
*/
const Modif &
Modif::accountMasses(double &mono, double &avg, int times) const
{
  mono += m_mono * times;
  avg  += m_avg * times;

  return *this;
}

/*!
\brief Returns the mass of the type defined by \a mass_type.
*/
double
Modif::getMass(Enums::MassType mass_type)
{
  if(mass_type == Enums::MassType::MONO)
    return m_mono;
  else if(mass_type == Enums::MassType::AVG)
    return m_avg;
  else
    qFatal() << "Not possible to ask a mass that is not MONO nor AVG";

  return -1;
}

/*!
\brief Parses the modification XML \a element specifically for \a version.

Parses the modif \c mdf XML element passed as argument and for each
encountered data will set the data to this modif (this is
called XML rendering).The parsing is delegated to a function that is
specific for \a version of the polymer chemistry definition.

The \c mdf XML element is found in the polymer chemistry definition and has
the following form:


\code
<mdf>
<name>Acetylation</name>
<formula>C2H2O1</formula>
<targets>;K;</targets>
<maxcount>1</maxcount>
</mdf>
<mdf>
<name>AmidationAsp</name>
<formula>H1N1-O1</formula>
<targets>;D;</targets>
<maxcount>1</maxcount>
</mdf>
\endcode

After setting all the data, this modification calculates it masses and
validates itself. If any of these steps fails, the error is reported
by returning false.

Returns true if parsing was successful, false otherwise.
*/
bool
Modif::renderXmlMdfElement(const QDomElement &element,
                           [[maybe_unused]] int version)
{
  //  Assume this,  we later negate this as we go.
  m_isValid = true;

  // QDomDocument doc;
  // QDomElement root = doc.importNode(element, true).toElement();
  // doc.appendChild(root);
  // qDebug() << "The element:" << doc.toString();

  if(element.tagName() != "mdf")
    {
      qDebug() << "The element tag name is not 'mdf'";
      m_isValid = false;
      return m_isValid;
    }

  QDomElement child;

  child = element.firstChildElement("name");

  if(child.isNull() || child.text().isEmpty())
    {
      qDebug() << "The Modif did not render correctly: problem with the "
                  "<name> element.";
      m_isValid = false;
      return m_isValid;
    }
  m_name = child.text();
  // qDebug() << "The name:" << m_name;

  child = child.nextSiblingElement("formula");

  if(child.isNull())
    {
      qDebug() << "The Modif did not render correctly: problem with the "
                  "<formula> element.";
      m_isValid = false;
      return m_isValid;
    }

  Formula temp_formula(nullptr);

  if(!temp_formula.renderXmlFormulaElement(child))
    {
      qDebug() << "The Modif did not render correctly: the formula did not "
                  "render correctly.";
      m_isValid = false;
      return m_isValid;
    }
  m_formula = temp_formula.getActionFormula(/*with_title*/ true);
  // qDebug() << "The formula:" << m_formula;

  child = child.nextSiblingElement("targets");

  if(child.isNull() || child.text().isEmpty())
    {
      qDebug() << "The Modif did not render correctly: problem with the "
                  "<targets> element.";
      m_isValid = false;
      return m_isValid;
    }
  m_targets = child.text();

  bool ok = false;

  QString targets_after_validation =
    validateTargets(m_targets, /* simplify */ false, ok);

  // qDebug() << "The targets after validation:" << targets_after_validation;

  if(!ok)
    {
      qDebug() << "The Modif did not render correctly: the targets could "
                  "not validate successfully.";
      m_isValid = false;
      return m_isValid;
    }
  m_targets = targets_after_validation;

  child = child.nextSiblingElement("maxcount");

  if(child.isNull() || child.text().isEmpty())
    {
      qDebug() << "The Modif did not render correctly: problem with the "
                  "<maxcount> element.";
      m_isValid = false;
      return m_isValid;
    }
  QString max_count_string = child.text();

  ok = false;

  int max_count_int = max_count_string.toInt(&ok);

  if(!ok || max_count_int <= 0)
    {
      qDebug() << "The Modif did not render correctly: the maxcount value "
                  "is less than 0.";
      m_isValid = false;
      return m_isValid;
    }
  m_maxCount = max_count_int;

  ErrorList error_list;
  m_isValid = validate(&error_list);
  if(!m_isValid)
    {
      qDebug() << "The Modif did not validate successfully after "
                  "rendering, with errors:";
      Utils::joinErrorList(error_list, ", ");
    }
  else
    {
      //  At this point,  because we are creating a Modif from scratch,
      //  and not by copying or by assignment, we calculate the masses
      //  explicitely (validate() does that but not on member m_mono/m_avg
      //  because the method is const.).

      if(!calculateMasses(nullptr))
        {
          qDebug() << "The Modif's masses could not be calculated.";
        }
    }

  qDebug() << "Correctly rendered Modif" << m_name;

  return m_isValid;
}

/*!
\brief Formats this modification's data as a string suitable to be used as a
\c mdf XML element in the polymer chemistry definition or a polymer sequence
file.

The typical modification element that is generated in this function looks
like this:

\code
<mdf>
<name>Acetylation</name>
<formula>C2H2O1</formula>
<targets>;K;</targets>
<maxcount>1</maxcount>
</mdf>
<mdf>
<name>AmidationAsp</name>
<formula>H1N1-O1</formula>
<targets>;D;</targets>
<maxcount>1</maxcount>
</mdf>
\endcode

The formatting of the XML element takes into account \a offset and \a
indent by prepending the string with \a offset * \a indent character
substring.

\a indent defaults to two spaces.

Returns a string.
*/
QString
Modif::formatXmlMdfElement(int offset, const QString &indent) const
{
  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 <modif> node that should look like this:

  <mdf>
  <name>Phosphorylation</name>
  <formula>-H+H2PO3</formula>
  <targets>S;T;Y</targets>
  <maxcount>1</maxcount>
  </mdf>

  */

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

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

  // Continue with indented elements.

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

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

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

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

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

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

  // QString debug_message =
  //   QString("%1\n%2\n").arg("Returning string:").arg(text);
  // qDebug().noquote() << debug_message;

  return text;
}

/*!
\brief Resets this modification to an empty object.
*/
void
Modif::clear()
{
  m_name.clear();
  m_formula.clear();
  m_targets.clear();
  m_maxCount = -1;

  m_mono = 0;
  m_avg  = 0;

  m_isValid = false;
}

/*!
\brief Returns a string representing this Modif instance.
*/
QString
Modif::toString() const
{
  return QString("%1, %2, %3, %4")
    .arg(m_name)
    .arg(m_formula)
    .arg(m_targets)
    .arg(m_maxCount);
}

} // namespace libXpertMassCore
} // namespace MsXpS
