/* 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 MsXpS/libXpertMassCore/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 the
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 action-formula: "-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 with a number of parameters.

\list

\li \a pol_chem_def_csp: initializes mcsp_polChemDef, the Polymer chemistry
definition.
\li \a name: initializes m_name.

\li \a left_code: initializes m_leftCode, the \l Monomer code at the left of the
cleavage site.

\li \a left_formula: initializes m_leftFormula, the \l Formula to be applied
onto the left monomer code.

\li \a right_code: initializes m_rightCode, the \l Monomer code at the right of
the cleavage site.

\li \a right_formula: initializes m_rightFormula, the \l Formula to be applied
onto the right monomer code.

\endlist

After setting the member data, the instance is validated and m_isValid is set to
the result of that validation.
*/
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 m_isValid is set to
the result of that validation.
*/
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 m_isValid is set to
the result of that validation.
*/
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 mcsp_polChemDef PolChemDef member.
*/
PolChemDefCstSPtr
CleavageRule::getPolchemDefCstSPtr() const
{
  return mcsp_polChemDef;
}

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

After setting the member data, the instance is validated and m_isValid is set to
the result of that validation.
*/
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.

This is the code of the Monomer that sits left of the cleavage site. Upon the
actual cleavage, this \l{Monomer} will be the right-end
(Enums::PolymerEnd::RIGHT) Monomer of the left Oligomer.

After setting the member data, the instance is validated and m_isValid is set to
the result of that validation.
*/
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.

This is the code of the Monomer that sits right of the cleavage site. Upon the
actual cleavage, this \l{Monomer} will be the left-end (Enums::PolymerEnd::LEFT)
Monomer of the right Oligomer.

After setting the member data, the instance is validated and m_isValid is set to
the result of that validation.
*/
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 m_leftFormula member to \a formula.

After setting the member data, the instance is validated and m_isValid is set to
the result of that validation.
*/
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 m_rightFormula member to \a formula.

After setting the member data, the instance is validated and m_isValid is set to
the result of that validation.
*/
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 \a other to this CleavageRule instance.

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

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 instance and \a other are identical.

\sa operator!=()
*/
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 instance and \a other are different.

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

  return !operator==(other);
}

//////////////// VALIDATIONS /////////////////////
/*!
\brief Returns true if validation of this CleavageRule instance was successful,
false otherwise.

The validation process encompasses the following steps:

\list

\li The member mcsp_polChemDef cannot be nullptr.

\li The member m_name name cannot be empty;

\li If the member m_leftCode left monomer code is not empty, it must be known to
the polymer chemistry definition. In that case, if the member m_leftFormula 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

If errors are encountered and \a error_list_p is not nullptr, these errors
are stored in that ErrorList.
*/
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, the member data are updated from the parsed
\a element entities and validated along the process, thus essentially
initializing this instance.

A typical \a element reads like this:

\code

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

\endcode

Returns true if parsing and validation were successful, false otherwise.

\sa formatXmlClrElement()
*/
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>Met</le-mnm-code>
   *     <le-formula>-C1H2S1+O1</le-formula>
   *     <re-mnm-code>Fake</re-mnm-code>
   *     <re-formula>-FAKE1+FAKE1</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.

A typical element reads like this:

\code

<clr>
<name>Homoseryl</name>
<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 the formatted string.

\sa renderXmlClrElement()
*/
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
