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


/////////////////////// Local includes
#include "MsXpS/libXpertMassCore/CleavageRule.hpp"
#include "MsXpS/libXpertMassCore/PolChemDef.hpp"

namespace MsXpS
{
namespace libXpertMassCore
{


/*!
\class MsXpS::libXpertMassCore::CleavageRule
\inmodule libXpertMassCore
\ingroup PolChemDefAqueousChemicalReactions
\inheaderfile CleavageRule.hpp

\brief The CleavageRule class provides a model for specifying aqueous cleavage
rules for refining cleavage agent specifications (\l CleavageAgent) of
\l{Polymer} \l{Sequence}s.

Cleavage rules help refine the description of the chemical reaction that is the
basis of a cleavage (either enzymatic or chemical).

While a number of cleavage agents (like a number of enzymes) do not make
unexpected reactions upon the cleavage (enzymes usually hydrolyze their
substrates), there are chemical agents that during the process of cleaving their
polymer sequence substrate also chemically modify the ends of the generated
oligomers. One notorious example is the case of cyanogen bromide, that cleaves
proteins right (that is, C-terminal) of methionyl residues. Upon such cleavage,
the monomer at the right side of the generated oligomer (methionyl residue) gets
modified according to this actionformula: "-CH2S+O". This reaction is modelled
using a CleavageRule.

\sa CleavageMotif, CleavageAgent
*/


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

\brief The \l PolChemDef polymer chemistry definition.
*/

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

\brief The name of the CleavageRule.
*/

/*!
\variable MsXpS::libXpertMassCore::CleavageRule::m_leftCode

\brief The \l Monomer code at the left of the cleavage site.
*/

/*!
\variable MsXpS::libXpertMassCore::CleavageRule::m_leftFormula

\brief The \l Formula to be applied onto the left monomer code.
*/

/*!
\variable MsXpS::libXpertMassCore::CleavageRule::m_rightCode

\brief The \l Monomer code at the right of the cleavage site.
*/

/*!
\variable MsXpS::libXpertMassCore::CleavageRule::m_rightFormula

\brief The \l Formula to be applied onto the right monomer code.
*/

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

\brief The validity status of the CleavageRule instance.
*/


/*!
\brief Constructs a CleavageRule instance with \a pol_chem_def_csp as the
polymer chemistry definition.

The validiy status (m_isValid) of this install will be set to false.
*/
CleavageRule::CleavageRule(PolChemDefCstSPtr pol_chem_def_csp)
  : mcsp_polChemDef(pol_chem_def_csp)
{
}

/*!
\brief Constructs a CleavageRule instance

\list
\li \a pol_chem_def_csp: Polymer chemistry definition.
\li \a name: the name.
\li \a left_code: The \l Monomer code at the left of the cleavage site.
\li \a left_formula: .The \l Formula to be applied onto the left monomer code.
\li \a right_code: .The \l Monomer code at the right of the cleavage site.
\li \a right_formula: .The \l Formula to be applied onto the right monomer code.
\endlist

After setting the member data, the instance is validated and the result is set
to m_isValid.
*/
CleavageRule::CleavageRule(PolChemDefCstSPtr pol_chem_def_csp,
                           const QString &name,
                           const QString &left_code,
                           const QString &left_formula,
                           const QString &right_code,
                           const QString &right_formula)
  : mcsp_polChemDef(pol_chem_def_csp),
    m_name(name),
    m_leftCode(left_code),
    m_leftFormula(left_formula),
    m_rightCode(right_code),
    m_rightFormula(right_formula)
{
  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug() << "Upon construction of CleavageRule,  the instance failed to "
                "validate with errors:"
             << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Constructs a CleavageRule instance as a copy of \a other.

After setting the member data, the instance is validated and the result is set
to m_isValid.
*/
CleavageRule::CleavageRule(const CleavageRule &other)
  : mcsp_polChemDef(other.mcsp_polChemDef),
    m_name(other.m_name),
    m_leftCode(other.m_leftCode),
    m_leftFormula(other.m_leftFormula),
    m_rightCode(other.m_rightCode),
    m_rightFormula(other.m_rightFormula)
{
  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug()
      << "Upon copy-construction of CleavageRule,  the instance failed to "
         "validate with errors:"
      << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Destructs this CleavageRule instance
*/
CleavageRule::~CleavageRule()
{
}

/*!
\brief Sets the PolChemDef to \a pol_chem_def_csp.

After setting the member data, the instance is validated and the result is set
to m_isValid.
*/
void
CleavageRule::setPolchemDefCstSPtr(PolChemDefCstSPtr pol_chem_def_csp)
{
  mcsp_polChemDef = pol_chem_def_csp;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug()
      << "Upon setting PolChemDef of CleavageRule, the instance failed to "
         "validate with errors:"
      << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Returns the PolChemDef.
*/
PolChemDefCstSPtr
CleavageRule::getPolchemDefCstSPtr() const
{
  return mcsp_polChemDef;
}

/*!
\brief Sets the name to \a name.

After setting the member data, the instance is validated and the result is set
to m_isValid.
*/
void
CleavageRule::setName(const QString &name)
{
  m_name = name;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug() << "Upon setting name of CleavageRule, the instance failed to "
                "validate with errors:"
             << Utils::joinErrorList(error_list, ", ");
}

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

/*!
\brief Sets the left \a code.

After setting the member data, the instance is validated and the result is set
to m_isValid.
*/
void
CleavageRule::setLeftCode(const QString &code)
{
  m_leftCode = code;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug()
      << "Upon setting left code of CleavageRule, the instance failed to "
         "validate with errors:"
      << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Returns the left code.
*/
const QString &
CleavageRule::getLeftCode() const
{
  return m_leftCode;
}

/*!
\brief Sets the right \a code.

After setting the member data, the instance is validated and the result is set
to m_isValid.
*/
void
CleavageRule::setRightCode(const QString &code)
{
  m_rightCode = code;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug()
      << "Upon setting right code of CleavageRule, the instance failed to "
         "validate with errors:"
      << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Returns the right code.
*/
const QString &
CleavageRule::getRightCode() const
{
  return m_rightCode;
}

/*!
\brief Sets the left \a formula.

After setting the member data, the instance is validated and the result is set
to m_isValid.
*/
void
CleavageRule::setLeftFormula(const Formula &formula)
{
  m_leftFormula = formula;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug()
      << "Upon setting left formula of CleavageRule, the instance failed to "
         "validate with errors:"
      << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Returns the left formula.
*/
const Formula &
CleavageRule::getLeftFormula() const
{
  return m_leftFormula;
}

/*!
\brief Sets the right \a formula.

After setting the member data, the instance is validated and the result is set
to m_isValid.
*/
void
CleavageRule::setRightFormula(const Formula &formula)
{
  m_rightFormula = formula;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug()
      << "Upon setting right formula of CleavageRule, the instance failed to "
         "validate with errors:"
      << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Returns the right formula.
*/
const Formula &
CleavageRule::getRightFormula() const
{
  return m_rightFormula;
}

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

After setting the member data, the instance is validated and the result is set
to m_isValid.

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

  mcsp_polChemDef = other.mcsp_polChemDef;
  m_name          = other.m_name;

  m_leftCode    = other.m_leftCode;
  m_leftFormula = other.m_leftFormula;

  m_rightCode    = other.m_rightCode;
  m_rightFormula = other.m_rightFormula;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug() << "Upon assignment of CleavageRule, the instance failed to "
                "validate with errors:"
             << Utils::joinErrorList(error_list, ", ");

  return *this;
}

/*!
\brief Returns true if \c this and \a other are identical.
*/
bool
CleavageRule::operator==(const CleavageRule &other) const
{
  if(this == &other)
    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_leftCode == other.m_leftCode &&
         m_leftFormula == other.m_leftFormula &&
         m_rightCode == other.m_rightCode &&
         m_rightFormula == other.m_rightFormula;
}

/*!
\brief Returns true if \c this and \a other are different.
*/
bool
CleavageRule::operator!=(const CleavageRule &other) const
{
  if(this == &other)
    return false;

  return !operator==(other);
}

//////////////// VALIDATIONS /////////////////////
/*!
\brief Validates this CleavageRule instance and stores error messages to \a
error_list_p.

Validation entails the following:

\list

\li The member PolChemDef cannot be nullptr;

\li The member name cannot be empty;

\li If the left monomer code is not empty, it must be known to the polymer
chemistry definition. In that case, if the left formula is not empty, it needs
to validate successfully.

\li The same logic is applied to the monomer at the right hand side of the
cleavage site.
\endlist

Sets m_isValid to true if the validation is successful, false otherwise.
Returns m_isValid.
*/
bool
CleavageRule::validate(ErrorList *error_list_p) const
{
  m_isValid = true;

  if(mcsp_polChemDef == nullptr || mcsp_polChemDef.get() == nullptr)
    {
      qDebug() << "A CleavageRule with no PolChemDef available cannot "
                  "validate successfully.";

      m_isValid = false;

      if(error_list_p)
        error_list_p->push_back(
          "A CleavageRule with no PolChemDef available cannot validate "
          "successfully");
    }

  if(m_name.isEmpty())
    {
      qDebug() << "A CleavageRule with no name cannot validate successfully.";

      m_isValid = false;

      if(error_list_p)
        error_list_p->push_back(
          "A CleavageRule with no name cannot validate successfully");
    }

  if(m_leftCode.isEmpty() && !m_leftFormula.getActionFormula().isEmpty())
    {
      qDebug()
        << "A CleavageRule without a left monomer code but with a left formula"
           "cannot validate successfully.";

      m_isValid = false;

      if(error_list_p)
        error_list_p->push_back(
          "A CleavageRule without a left monomer code but with a left formula "
          "cannot validate "
          "successfully");
    }

  if(!m_leftCode.isEmpty())
    {
      if(mcsp_polChemDef != nullptr && mcsp_polChemDef.get() != nullptr &&
         mcsp_polChemDef->getMonomerCstSPtrByCode(m_leftCode) == nullptr)
        {
          qDebug() << "A CleavageRule with an unknown left monomer code "
                      "cannot validate successfully.";

          m_isValid = false;

          if(error_list_p)
            error_list_p->push_back(
              "A CleavageRule with an unknown left monomer code cannot "
              "validate "
              "successfully");
        }

      if(m_leftFormula.getActionFormula().isEmpty())
        {
          qDebug() << "A CleavageRule with an empty left formula cannot "
                      "validate successfully.";

          m_isValid = false;

          if(error_list_p)
            error_list_p->push_back(
              "A CleavageRule with an empty left formula cannot validate "
              "successfully");
        }
      else if(mcsp_polChemDef != nullptr && mcsp_polChemDef.get() != nullptr &&
              !m_leftFormula.validate(mcsp_polChemDef->getIsotopicDataCstSPtr(),
                                      error_list_p))
        {
          qDebug() << "A CleavageRule with an invalid left formula cannot "
                      "validate successfully.";

          m_isValid = false;

          if(error_list_p)
            error_list_p->push_back(
              "A CleavageRule with an invalid left formula cannot validate "
              "successfully");
        }
    }

  if(m_rightCode.isEmpty() && !m_rightFormula.getActionFormula().isEmpty())
    {
      qDebug() << "A CleavageRule without a right monomer code but with a "
                  "right formula"
                  "cannot validate successfully.";

      m_isValid = false;

      if(error_list_p)
        error_list_p->push_back(
          "A CleavageRule without a right monomer code but with a right "
          "formula "
          "cannot validate "
          "successfully");
    }

  if(!m_rightCode.isEmpty())
    {
      if(mcsp_polChemDef != nullptr && mcsp_polChemDef.get() != nullptr &&
         mcsp_polChemDef->getMonomerCstSPtrByCode(m_rightCode) == nullptr)
        {
          qDebug() << "A CleavageRule with an unknown right monomer code "
                      "cannot validate successfully.";

          m_isValid = false;

          if(error_list_p)
            error_list_p->push_back(
              "A CleavageRule with an unknown right monomer code cannot "
              "validate "
              "successfully");
        }

      if(m_rightFormula.getActionFormula().isEmpty())
        {
          qDebug() << "A CleavageRule with an empty right formula cannot "
                      "validate successfully.";

          m_isValid = false;

          if(error_list_p)
            error_list_p->push_back(
              "A CleavageRule with an empty right formula cannot validate "
              "successfully");
        }
      else if(mcsp_polChemDef != nullptr && mcsp_polChemDef.get() != nullptr &&
              !m_rightFormula.validate(
                mcsp_polChemDef->getIsotopicDataCstSPtr(), error_list_p))
        {
          qDebug() << "A CleavageRule with an invalid right formula cannot "
                      "validate successfully.";

          m_isValid = false;

          if(error_list_p)
            error_list_p->push_back(
              "A CleavageRule with an invalid right formula cannot validate "
              "successfully");
        }
    }

  return m_isValid;
}

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

//////////////// XML DATA LOADING WRITING /////////////////////
/*!
\brief Parses the CleavageRule XML \a element using a \a{version}ed function.

Upon parsing of the \a element, its data are validated and set to this
CleavageRule instance, thus essentially initializing it.

Returns true if parsing and validation were successful, false otherwise.
*/
bool
CleavageRule::renderXmlClrElement(const QDomElement &element,
                                  [[maybe_unused]] int version)
{
  QDomElement child;

  bool leftCodeSet     = false;
  bool leftFormulaSet  = false;
  bool rightCodeSet    = false;
  bool rightFormulaSet = false;

  /* The xml node we are in is structured this way:
   *
   *  <clr>
   *     <name>Homeseryl</name>
   *     <le-mnm-code>M</le-mnm-code>
   *     <le-formula>-C1H2S1+O1</le-formula>
   *     <re-mnm-code>M</re-mnm-code>
   *     <re-formula>-C1H2S1+O1</re-formula>
   *  </clr>
   *
   * And the element parameter points to the
   *
   * <clr> element tag:
   *  ^
   *  |
   *  +----- here we are right now.
   *
   * Which means that xml_node->name == "clr" and that
   * we'll have to go one step down to the first child of the
   * current node in order to get to the <code> element.
   *
   * Note that the DTD stipulates that there can be no or one at most
   * of each left end and/or right end set of data. So be careful
   * with the assertions !
   * This is the DTD material:
   * <!ELEMENT clr((le-mnm-code,le-formula)?,
   *(re-mnm-code,re-formula)?)>
   */

  if(element.tagName() != "clr")
    return false;

  child = element.firstChildElement();

  if(version == 1)
    {
      // no-op

      version = 1;
    }

  if(child.tagName() != "name")
    return false;

  m_name = child.text();

  child = child.nextSiblingElement();

  while(!child.isNull())
    {
      // OK, apparently there is a child element, so let's try to see
      // what's going on. It can either be "le-mnm-code" or "re-mnm-code".

      if(child.tagName() == "le-mnm-code")
        {
          m_leftCode  = child.text();
          leftCodeSet = true;
        }
      else if(child.tagName() == "le-formula")
        {
          m_leftFormula.setActionFormula(child.text());
          leftFormulaSet = true;
        }
      else if(child.tagName() == "re-mnm-code")
        {
          m_rightCode  = child.text();
          rightCodeSet = true;
        }
      else if(child.tagName() == "re-formula")
        {
          m_rightFormula.setActionFormula(child.text());
          rightFormulaSet = true;
        }

      child = child.nextSiblingElement();
    }

  // OK, we just finished parsing this <clr> element. Check what we
  // got.

  if(leftCodeSet)
    {
      if(!leftFormulaSet)
        return false;
    }

  if(rightCodeSet)
    {
      if(!rightFormulaSet)
        return false;
    }

  // It cannot be that no single code could be set.
  if(!leftCodeSet && !rightCodeSet)
    return false;

  ErrorList error_list;
  m_isValid = validate(&error_list);
  if(!m_isValid)
    {
      qDebug() << "The CleavageRule rendered from <clr> element is invalid.";
    }

  qDebug() << "The <clr> element was rendered successfully.";

  return m_isValid;
}

/*!
\brief Formats a string representing this CleavageRule instance suitable to use
as an XML element.

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

\code
<clr>
<re-mnm-code>M</re-mnm-code>
<re-formula>-CH2S+O</re-formula>
</clr>
\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
CleavageRule::formatXmlClrElement(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;
    }

  /*
  <clr>
  <re-mnm-code>M</re-mnm-code>
  <re-formula>-CH2S+O</re-formula>
  </clr>
  */

  text += QString("%1<clr>\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);

  if(!m_leftCode.isEmpty())
    {
      Q_ASSERT(!m_leftFormula.getActionFormula().isEmpty());

      text +=
        QString("%1<le-mnm-code>%2</le-mnm-code>\n").arg(lead).arg(m_leftCode);

      text += QString("%1<le-formula>%2</le-formula>\n")
                .arg(lead)
                .arg(m_leftFormula.getActionFormula());
    }

  if(!m_rightCode.isEmpty())
    {
      Q_ASSERT(!m_rightFormula.getActionFormula().isEmpty());

      text +=
        QString("%1<re-mnm-code>%2</re-mnm-code>\n").arg(lead).arg(m_rightCode);

      text += QString("%1<re-formula>%2</re-formula>\n")
                .arg(lead)
                .arg(m_rightFormula.getActionFormula());
    }

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

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

  return text;
}


} // namespace libXpertMassCore
} // namespace MsXpS
