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


/////////////////////// Std includes
#include <cmath>


/////////////////////// Local includes
#include "MsXpS/libXpertMassCore/PkaPhPi.hpp"

namespace MsXpS
{
namespace libXpertMassCore
{


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

\brief The PkaPhPi class provides a model for specifying the
acido-basic properties of a chemical entity.
*/


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

\brief The polymer chemistry definition.
*/

/*!
\variable MsXpS::libXpertMassCore::PkaPhPi::m_monomers

\brief The container of \l Monomer instances as read from the pka_ph_pi.xml
file.
*/

/*!
\variable MsXpS::libXpertMassCore::PkaPhPi::m_modifs

\brief The container of \l Modif instances as read from the pka_ph_pi.xml file.
*/

/*!
\variable MsXpS::libXpertMassCore::PkaPhPi::m_ph

\brief The pH of the environment.

This pH value is required to compute the number of charges of a given chemical
entity (a \l Polymer) sequence, for example.
*/

/*!
\variable MsXpS::libXpertMassCore::PkaPhPi::m_pi

\brief The pI of the chemical entity.
*/

/*!
\variable MsXpS::libXpertMassCore::PkaPhPi::mcsp_polymer

\brief The polymer of which the acidobasic properties are computed.
*/

/*!
\variable MsXpS::libXpertMassCore::PkaPhPi::m_calcOptions

\brief The \l CalcOptions that configure the way the computations are to be
carried out.
*/

/*!
\variable MsXpS::libXpertMassCore::PkaPhPi::m_positiveCharges

\brief The count of positive charges.
*/

/*!
\variable MsXpS::libXpertMassCore::PkaPhPi::m_negativeCharges

\brief The count of negative charges.
*/

/*!
\brief Constructs a PkaPhPi instance with a number of parameters.

\list

\li \a pol_chem_def_csp: The polymer chemistry definition

\li \a polymer_cqsp: The polymer within the context of which the calculations
are performed.

\li \a calc_options: The options driving the calculations.

\endlist
*/
PkaPhPi::PkaPhPi(PolChemDefCstSPtr pol_chem_def_csp,
                 PolymerCstQSPtr polymer_cqsp,
                 const CalcOptions &calc_options)
  : mcsp_polChemDef(pol_chem_def_csp),
    mcsp_polymer(polymer_cqsp),
    m_calcOptions(calc_options)
{
  if(mcsp_polChemDef == nullptr || mcsp_polChemDef.get() == nullptr)
    qDebug()
      << "Creating PkaPhPi instance without polymer chemistry definition.";

  if(polymer_cqsp == nullptr)
    qFatal() << "Programming error. Pointer cannot be nullptr.";
}

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

/*!
\brief Sets the PolChemDef to \a pol_chem_def_csp.
*/
void
PkaPhPi::setPolChemDefCstSPtr(PolChemDefCstSPtr pol_chem_def_csp)
{
  mcsp_polChemDef = pol_chem_def_csp;

  if(mcsp_polChemDef == nullptr || mcsp_polChemDef.get() == nullptr)
    qDebug() << "Setting an Creating PkaPhPi instance without polymer "
                "chemistry definition.";
}

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

/*!
\brief Moves the Monomer instances from \a monomers to the member container.
*/
void
PkaPhPi::setMonomers(std::vector<MonomerCstSPtr> &&monomers)
{
  m_monomers.clear();
  m_monomers = std::move(monomers);
}

/*!
\brief Moves the Monomer instances from \a monomers to the member container.
*/
void
PkaPhPi::setMonomers(std::vector<MonomerSPtr> &&monomers)
{
  //  We want the pointers to be to const Monomer.

  m_monomers.clear();
  m_monomers.reserve(monomers.size());

  std::transform(std::make_move_iterator(monomers.begin()),
                 std::make_move_iterator(monomers.end()),
                 std::back_inserter(m_monomers),
                 [](std::shared_ptr<Monomer> &&monomer_sp) {
                   return std::const_pointer_cast<const Monomer>(
                     std::move(monomer_sp));
                 });

  monomers.clear();
}

/*!
\brief Copies the Monomer instances in \a monomers to the member container.

This is a shallow copy with only the pointers being copied.
*/
void
PkaPhPi::setMonomers(std::vector<MonomerCstSPtr> &monomers)
{
  m_monomers = monomers;
}

/*!
\brief Returns a const reference to the container of Monomer instances.
*/
const std::vector<MonomerCstSPtr> &
PkaPhPi::getMonomersCstRef() const
{
  return m_monomers;
}

/*!
\brief Moves the Modif instances from \a modifs to the member container.
*/
void
PkaPhPi::setModifs(std::vector<ModifCstSPtr> &&modifs)
{
  m_modifs.clear();
  m_modifs = std::move(modifs);
  modifs.clear();
}

/*!
\brief Moves the Modif instances from \a modifs to the member container.
*/
void
PkaPhPi::setModifs(std::vector<ModifSPtr> &&modifs)
{
  //  We want the pointers to be to const Modif.

  m_modifs.clear();
  m_modifs.reserve(modifs.size());

  std::transform(std::make_move_iterator(modifs.begin()),
                 std::make_move_iterator(modifs.end()),
                 std::back_inserter(m_modifs),
                 [](std::shared_ptr<Modif> &&modif_sp) {
                   return std::const_pointer_cast<const Modif>(
                     std::move(modif_sp));
                 });

  modifs.clear();
}

/*!
\brief Copies the Modif instances in \a modifs to the member container.

This is a shallow copy with only the pointers being copied.
*/
void
PkaPhPi::setModifs(std::vector<ModifCstSPtr> &modifs)
{
  m_modifs = modifs;
}

/*!
\brief Returns a const reference to the container of Modif instances.
*/
const std::vector<ModifCstSPtr> &
PkaPhPi::getModifs() const
{
  return m_modifs;
}

/*!
\brief Sets the pH to \a ph.
*/
void
PkaPhPi::setPh(double ph)
{
  Q_ASSERT(ph > 0 && ph < 14);

  m_ph = ph;
}

/*!
\brief Returns the pH.
*/
double
PkaPhPi::ph()
{
  return m_ph;
}

/*!
\brief Returns the pI.
*/
double
PkaPhPi::pi()
{
  return m_pi;
}

/*!
\brief Returns the positive charges.
*/
double
PkaPhPi::positiveCharges()
{
  return m_positiveCharges;
}

/*!
\brief Returns the negative charges.
*/
double
PkaPhPi::negativeCharges()
{
  return m_negativeCharges;
}

/*!
\brief Sets the calculation options to \a calc_options.
*/
void
PkaPhPi::setCalcOptions(const libXpertMassCore::CalcOptions &calc_options)
{
  m_calcOptions.initialize(calc_options);
}

/*!
\brief Calculates the charges (positive and negative).

The general scheme is :

Get the list of the iter_index_range of the different \l Polymer region
selections. For each first monomer and end monomer of a given region selection,
check if the the region is an oligomer or a residual chain (m_selectionType of
libXpertMassCore::CalcOptions); act accordingly. Also, check for each selection
region if it encompasses the polymer left/right end. If the left/right end
modifications are to be taken into account, act accordingly.

The positive and negative charges are stored in the member \l m_positiveCharges
and \l m_negativeCharges variables.

Returns the count of chemical groups that have been processed.

\sa calculatePi()
*/
int
PkaPhPi::calculateCharges()
{
  int processedChemicalGroups = 0;

  m_positiveCharges = 0;
  m_negativeCharges = 0;

  // We of course need monomers ! Instead, we may not need modifs.
  if(mcsp_polChemDef == nullptr || mcsp_polChemDef.get() == nullptr)
    return -1;

  std::size_t polymerSize = mcsp_polymer->size();

  const IndexRangeCollection &index_range_collection =
    m_calcOptions.getIndexRangeCollectionCstRef();

  for(qsizetype iter = 0; iter < index_range_collection.size(); ++iter)
    {
      const IndexRange &iter_index_range =
        index_range_collection.getRangeCstRefAt(iter);

      qsizetype start_index = iter_index_range.m_start;
      qsizetype stop_index  = iter_index_range.m_stop;

      bool is_left_most_sequence_range =
        index_range_collection.isLeftMostIndexRange(iter_index_range);
      bool is_right_most_sequence_range =
        index_range_collection.isRightMostIndexRange(iter_index_range);

      for(qsizetype jter = start_index; jter < stop_index + 1; ++jter)
        {
          MonomerSPtr monomer_csp =
            mcsp_polymer->getSequenceCstRef().getMonomerCstSPtrAt(jter);

          // Find a monomer by the same code in our list of monomers
          // that have been fed with chemical group data. Note that
          // all the monomers in a given sequence must not
          // necessarily have a counterpart in the local list of
          // monoemers. For example, there might be cases in which a
          // given monomer might not bring any charge whatsoever.

          QString code = monomer_csp->getCode();

          std::vector<MonomerCstSPtr>::const_iterator monomer_iterator_cst =
            std::find_if(m_monomers.cbegin(),
                         m_monomers.cend(),
                         [code](const MonomerCstSPtr &monomer_csp) {
                           return monomer_csp->getCode() == code;
                         });

          if(monomer_iterator_cst == m_monomers.cend())
            continue;

          // A monomer can have multiple such "CHEMICAL_GROUP"
          // properties. Indeed, for example for proteins, a monomer
          // might have three such chemical groups(and thus three
          // Prop objects): one for the alpha NH2, one for the
          // alpha COOH and one for a residual chain chemical group, like
          // epsilon NH2 for lysine.

          for(int kter = 0; kter < (*monomer_iterator_cst)->propList().size();
              ++kter)
            {
              Prop *prop = (*monomer_iterator_cst)->propList().at(kter);

              if(prop->name() != "CHEMICAL_GROUP")
                continue;

              // 		qDebug() << __FILE__ << __LINE__
              // 			  << "Monomer has property CHEMICAL_GROUP...";

              // Get the chemical group out of the property.

              ChemicalGroup *chemicalGroup =
                static_cast<ChemicalGroup *>(prop->data());

              if(static_cast<int>(chemicalGroup->getPolRule()) &
                 static_cast<int>(Enums::ChemicalGroupTrapped::LEFT))
                {
                  // 		    qDebug() << __FILE__ << __LINE__
                  // 			      << "... that is CHEMGROUP_LEFT_TRAPPED";

                  // The chemical group we are dealing with is trapped
                  // when the monomer is polymerized on the left end, that
                  // is if the monomer is not the left end monomer of the
                  // sequence being analyzed.

                  // Thus we only can take it into account if one of
                  // two conditions are met:

                  // 1. The monomer is the left end monomer of the
                  // whole polymer sequence.

                  // 2. The monomer is the left end monomer of the
                  // region selection AND the selection type is
                  // oligomers(thus it does not get polymerized to
                  // the previous selection region).

                  if(jter > 0)
                    {
                      // Clearly we are not dealing with the left
                      // end of the polymer, so check if we have to
                      // account for this chemical group or not.

                      if(!is_left_most_sequence_range)
                        {
                          // The current libXpertMassCore::Coordinates is not
                          // the left-most libXpertMassCore::Coordinates in the
                          // libXpertMassCore::CoordinateList, thus we cannot
                          // consider it to be the "left end
                          // iter_index_range" of the
                          // libXpertMassCore::CoordinateList. Just continue
                          // without exploring any more.
                          continue;
                        }
                      if(jter == start_index)
                        {
                          // The current monomer is the first
                          // monomer of libXpertMassCore::Coordinates. We only
                          // take into account the chemical group if each
                          // libXpertMassCore::Coordinates is to be considered
                          // an oligomer.

                          if(m_calcOptions.getSelectionType() !=
                             Enums::SelectionType::OLIGOMERS)
                            continue;
                        }
                    }
                }

              if(static_cast<int>(chemicalGroup->getPolRule()) &
                 static_cast<int>(Enums::ChemicalGroupTrapped::RIGHT))
                {
                  // 		    qDebug() << __FILE__ << __LINE__
                  // 			      << "... that is CHEMGROUP_RIGHT_TRAPPED";

                  // See explanations above.

                  if(jter < (qsizetype)polymerSize - 1)
                    {
                      // Clearly, we are not dealing with the right
                      // end of the polymer.

                      if(!is_right_most_sequence_range)
                        {
                          // The current libXpertMassCore::Coordinates is not
                          // the right-most libXpertMassCore::Coordinates of the
                          // libXpertMassCore::CoordinateList, thus we cannot
                          // consider it to be the "right end
                          // iter_index_range" of the
                          // libXpertMassCore::CoordinateList. Just continue
                          // without exploring anymore.
                          continue;
                        }
                      if(jter == stop_index)
                        {
                          // The current monomer is the last monomer
                          // of libXpertMassCore::Coordinates. We only take into
                          // account the chemical group if each
                          // libXpertMassCore::Coordinates is to be considered
                          // an oligomer(and not a residual chain).

                          if(m_calcOptions.getSelectionType() !=
                             Enums::SelectionType::OLIGOMERS)
                            continue;
                        }
                    }
                }

              if(iter == 0 &&
                 static_cast<int>(m_calcOptions.getPolymerEntities()) &
                   static_cast<int>(Enums::ChemicalEntity::LEFT_END_MODIF))
                {
                  // We are iterating in the monomer that is at the
                  // beginning of the polymer sequence. We should
                  // test if the chemical group we are dealing with
                  // right now has a special rule for the left end
                  // of the polymer sequence region.

                  int ret = accountPolymerEndModif(
                    Enums::ChemicalEntity::LEFT_END_MODIF, *chemicalGroup);
                  if(ret >= 0)
                    {
                      // 			qDebug() << __FILE__ << __LINE__
                      // 				  << "Accounted for left end modif.";

                      processedChemicalGroups += ret;
                      continue;
                    }
                }

              if(iter == (qsizetype)polymerSize - 1 &&
                 static_cast<int>(m_calcOptions.getPolymerEntities()) &
                   static_cast<int>(Enums::ChemicalEntity::RIGHT_END_MODIF))
                {
                  int ret = accountPolymerEndModif(
                    Enums::ChemicalEntity::RIGHT_END_MODIF, *chemicalGroup);
                  if(ret >= 0)
                    {
                      // 			qDebug() << __FILE__ << __LINE__
                      // 				  << "Accounted for right end modif.";

                      processedChemicalGroups += ret;
                      continue;
                    }
                }

              if(static_cast<int>(m_calcOptions.getMonomerEntities()) &
                   static_cast<int>(Enums::ChemicalEntity::MODIF) &&
                 (*monomer_iterator_cst)->isModified())
                {
                  int ret = accountMonomerModif(*(*monomer_iterator_cst),
                                                *chemicalGroup);
                  if(ret >= 0)
                    {
                      // 			qDebug() << __FILE__ << __LINE__
                      // 				  << "Accounted for monomer modif.";

                      processedChemicalGroups += ret;
                      continue;
                    }
                }

              double charge = calculateChargeRatio(
                chemicalGroup->getPka(), chemicalGroup->isAcidCharged());

              // 		qDebug() << __FILE__ << __LINE__
              // 			  << "Charge:" << charge;

              if(charge < 0)
                m_negativeCharges += charge;
              else if(charge > 0)
                m_positiveCharges += charge;

              // 		qDebug() << __FILE__ << __LINE__
              // 			  << "Pos =" << m_positiveCharges
              // 			  << "Neg = " << m_negativeCharges;

              ++processedChemicalGroups;
            }
          // End of
          // for (int kter = 0; kter < monomer->propList().size(); ++kter)

          // 	    qDebug() << __FILE__ << __LINE__
          // 		      << "End dealing with Monomer:" <<
          // seqMonomer->name()
          // 		      << "position:" << jter + 1;
        }
      // End of
      // for (int jter = start_index; jter < stop_index + 1; ++jter)

      // 	qDebug() << __FILE__ << __LINE__
      // 		  << "End dealing with libXpertMassCore::Coordinates";
    }
  // End of
  // for (int iter = 0; iter < index_range_collection.size(); ++iter)

  // We have finished processing all the libXpertMassCore::Coordinates in the
  // list.

  return processedChemicalGroups;
}

/*!
\brief Calculates the isoelectric point.

The isoelectric point is the pH at which a given analyte will have a net charge
of 0, that is, when the count of negative charges will be equal to the count of
positive charges.

The pI will be stored in the \l m_pi member variable.

Returns the count of chemical groups that have been processed.

\sa calculateCharges()
*/
int
PkaPhPi::calculatePi()
{
  int processedChemicalGroups = 0;
  int iteration               = 0;

  double netCharge   = 0;
  double firstCharge = 0;
  double thirdCharge = 0;

  // We of course need monomers ! Instead, we may not need modifs.
  if(!m_monomers.size())
    {
      m_pi = 0;
      m_ph = 0;

      return -1;
    }

  m_ph = 0;

  while(true)
    {
      //       qDebug() << "Current pH being tested:" << m_ph;

      processedChemicalGroups = calculateCharges();

      if(processedChemicalGroups == -1)
        {
          qDebug() << "Failed to calculate net charge for pH" << m_ph;

          m_pi = 0;
          m_ph = 0;

          return -1;
        }

      netCharge = m_positiveCharges + m_negativeCharges;

      // Note that if the 0.01 tested_ph step is enough to switch the
      // net charge from one excess value to another excess value in
      // the opposite direction, we'll enter an infinite loop.
      //
      // The evidence for such loop is that every other two measures,
      // the net charge of the polymer sequence will be the same.
      //
      // Here we test this so that we can break the loop.


      ++iteration;

      if(iteration == 1)
        {
          firstCharge = netCharge;
        }
      else if(iteration == 3)
        {
          thirdCharge = netCharge;

          if(firstCharge == thirdCharge)
            break;

          iteration = 0;

          firstCharge = netCharge;
        }

      // At this point we have to test the net charge:

      if(netCharge >= -0.1 && netCharge <= 0.1)
        {
          // 	  qDebug() << "Breaking loop with netCharge:" << netCharge;

          break;
        }

      if(netCharge > 0)
        {
          // The test ph is too low.

          m_ph += 0.01;
          // 	  qDebug() << "Set new pH m_ph += 0.01:" << m_ph
          // 		    << "netCharge:" << netCharge;

          continue;
        }

      if(netCharge < 0)
        {
          // The test ph is too high.

          m_ph -= 0.01;
          // 	  qDebug() << "Set new pH m_ph -= 0.01:" << m_ph
          // 		    << "netCharge:" << netCharge;

          continue;
        }
    }
  // End of
  // while(true)

  // At this point m_pi is m_ph.

  m_pi = m_ph;
  //   qDebug() << "pi is:" << m_pi;


  return processedChemicalGroups;
}

/*!
\brief Returns the ratio between the charged and the uncharged forms of the
chemical entity using \a pka and \a is_acid_charged. If the charge is negative,
the returned ratio is negative, positive otherwise.

The charged and uncharged species are the AH an A- species of the acido-basic
theory.

The calculation is based on the use of the m_ph member variable value.
*/
double
PkaPhPi::calculateChargeRatio(double pka, bool is_acid_charged)
{
  double aOverAh = 0;

  if(pka < 0)
    return 0;
  if(pka > 14)
    return 0;

  if(m_ph < 0)
    return 0;
  if(m_ph > 14)
    return 0;


  // Example with pKa = 4.25(Glu) ; pH = 4.16, thus we are more
  // acidic than pKa, we expect AH to be greater than A by a small
  // margin.

  aOverAh = (double)pow(10, (m_ph - pka));
  // aOverAh =  0.81283051616409951 (confirmed manually)

  if(aOverAh < 1)
    {
      /* The solution contains more acid forms(AH) than basic forms
        (A).
      */
      if(is_acid_charged)
        return (1 - aOverAh);
      else
        // The acid is not charged, that is, it is a COOH.
        // AH = 1 - A
        // A = aOverAh.AH
        // A = aOverAh.(1-A)
        // A = aOverAh - aOverAh.A
        // A(1+aOverAh) = aOverAh
        // A = aOverAh /(1+aOverAh), for us this is
        // A = 0.81283 / 1.81283 = 0.448

        // And not - aOverAh, that is - aOverAh.

        // Below seems faulty(20071204)
        // return(- aOverAh);

        // Tentative correction(20071204)
        return (-(aOverAh / (1 + aOverAh)));
    }
  else if(aOverAh > 1)
    {
      /* The solution contains more basic forms(A) than acid forms
        (AH).
      */
      if(is_acid_charged)
        return (1 / aOverAh);
      else
        return (-(1 - (1 / aOverAh)));
    }
  else if(aOverAh == 1)
    {
      /* The solution contains as many acid forms(AH) as basic forms
        (H).
      */
      if(is_acid_charged)
        return (aOverAh / 2);
      else
        return (-aOverAh / 2);
    }
  else
    qFatal("Programming error.");

  return 0;
}

/*!
\brief Accounts for the \a chemical_group according the the polymer chemical
entities defined in \a polymer_chem_ent.

A chemical group is described as follows:

\code
    <monomer>
      <code>C</code>
      <mnmchemgroup>
        <name>N-term NH2</name>
        <pka>9.6</pka>
        <acidcharged>TRUE</acidcharged>
        <polrule>left_trapped</polrule>
        <chemgrouprule>
          <entity>LE_PLM_MODIF</entity>
          <name>Acetylation</name>
          <outcome>LOST</outcome>
        </chemgrouprule>
      </mnmchemgroup>
      <mnmchemgroup>
        <name>C-term COOH</name>
        <pka>2.35</pka>
        <acidcharged>FALSE</acidcharged>
        <polrule>right_trapped</polrule>
      </mnmchemgroup>
      <mnmchemgroup>
        <name>Lateral SH2</name>
        <pka>8.3</pka>
        <acidcharged>FALSE</acidcharged>
        <polrule>never_trapped</polrule>
      </mnmchemgroup>
    </monomer>
\endcode

Returns the count of rules that were accounted for, or -1 if none was.
*/
int
PkaPhPi::accountPolymerEndModif(Enums::ChemicalEntity polymer_chem_ent,
                                const ChemicalGroup &chemical_group)
{
  QString modifName;
  ChemicalGroupRuleSPtr chemical_group_rule_sp = nullptr;
  int count                                    = 0;

  // Get the name of the modification of the polymer (if any) and get
  // the rule dealing with that polymer modification (if any).

  std::size_t chem_group_rule_index;

  if(static_cast<int>(polymer_chem_ent) &
     static_cast<int>(Enums::ChemicalEntity::LEFT_END_MODIF))
    {
      modifName              = mcsp_polymer->getLeftEndModifCstRef().getName();
      chemical_group_rule_sp = chemical_group.findRuleByEntityAndName(
        "LE_PLM_MODIF", modifName, chem_group_rule_index);
    }
  if(static_cast<int>(polymer_chem_ent) &
     static_cast<int>(Enums::ChemicalEntity::RIGHT_END_MODIF))
    {
      modifName              = mcsp_polymer->getRightEndModifCstRef().getName();
      chemical_group_rule_sp = chemical_group.findRuleByEntityAndName(
        "RE_PLM_MODIF", modifName, chem_group_rule_index);
    }
  else
    qFatal("Programming error.");


  // The polymer might not be modified, and also the chemical group
  // passed as parameter might not contain any rule about any polymer
  // modification. In that case we just have nothing to do.

  if(modifName.isEmpty())
    {
      if(chemical_group_rule_sp != nullptr)
        {
          double charge = calculateChargeRatio(chemical_group.getPka(),
                                               chemical_group.isAcidCharged());
          if(charge < 0)
            m_negativeCharges += charge;
          else if(charge > 0)
            m_positiveCharges += charge;

          return ++count;
        }
      else
        {
          // The polymer end was NOT modified and the chemical group
          // was NOT eligible for a polymer end modification. This
          // means that we do not have to process it, and we return -1
          // so that the caller function knows we did not do anything
          // and that this chemical group should continue to undergo
          // analysis without skipping it.

          return -1;
        }
    }
  // End of
  // if (modifName.isEmpty())

  if(chemical_group_rule_sp == nullptr)
    {
      // This chemical group was not "designed" to receive any polymer
      // end modification, so we have nothing to do with it and we
      // return -1 so that the caller function knows we did not do
      // anything and that this chemical group should continue to
      // undergo analysis without skipping it.

      return -1;
    }

  // At this point we know that the chemical group 'group' we are
  // analyzing is eligible for a polymer left end modification and
  // that it is indeed modified with a correcct modification. So we
  // have a rule for it. Let's continue the analysis.

  // Apparently the rule has data matching the ones we are looking
  // for. At this point we should now what action to take for this
  // group.

  if(chemical_group_rule_sp->getFate() == Enums::ChemicalGroupFate::LOST)
    {
      // We do not use the current chemical group 'group' because the
      // polymer end's modification has abolished it.
    }
  else if(chemical_group_rule_sp->getFate() ==
          Enums::ChemicalGroupFate::PRESERVED)
    {
      double charge = calculateChargeRatio(chemical_group.getPka(),
                                           chemical_group.isAcidCharged());
      if(charge < 0)
        m_negativeCharges += charge;
      else if(charge > 0)
        m_positiveCharges += charge;

      return ++count;
    }
  else
    qFatal("Programming error.");

  // Whatever we should do with the left/right end monomer's chemgroup,
  // we should take into account the modification itself that might
  // have brought chemgroup(s) worth calculating their intrinsic
  // charges!

  //  Find a modif object in the local list of modif objects, that has
  // the same name as the modification with which the left/right end
  // of the polymer is modified. We'll see what chemgroup(s) this
  // modification brings to the polymer sequence.

  std::vector<ModifCstSPtr>::const_iterator modif_iterator_cst =
    std::find_if(m_modifs.cbegin(),
                 m_modifs.cend(),
                 [modifName](const ModifCstSPtr &modif_csp) {
                   return modif_csp->getName() == modifName;
                 });

  if(modif_iterator_cst == m_modifs.cend())
    {
      //       qDebug() << __FILE__ << __LINE__
      // 		<< "Information: following modif not in local list:"
      // 		<< modifName;

      return count;
    }

  for(int jter = 0; jter < (*modif_iterator_cst)->propList().size(); ++jter)
    {
      Prop *prop = (*modif_iterator_cst)->propList().at(jter);

      if(prop->name() != "CHEMICAL_GROUP")
        continue;

      // Get the chemical group out of the property.

      const ChemicalGroup *chemicalGroup =
        static_cast<const ChemicalGroup *>(prop->data());

      double charge = calculateChargeRatio(chemicalGroup->getPka(),
                                           chemicalGroup->isAcidCharged());
      if(charge < 0)
        m_negativeCharges += charge;
      else if(charge > 0)
        m_positiveCharges += charge;

      ++count;
    }

  return count;
}

/*!
\brief Accounts for the \a chemical_group in the context of the \a monomer.

A chemical group is described as follows:

\code
    <monomer>
      <code>C</code>
      <mnmchemgroup>
        <name>N-term NH2</name>
        <pka>9.6</pka>
        <acidcharged>TRUE</acidcharged>
        <polrule>left_trapped</polrule>
        <chemgrouprule>
          <entity>LE_PLM_MODIF</entity>
          <name>Acetylation</name>
          <outcome>LOST</outcome>
        </chemgrouprule>
      </mnmchemgroup>
      <mnmchemgroup>
        <name>C-term COOH</name>
        <pka>2.35</pka>
        <acidcharged>FALSE</acidcharged>
        <polrule>right_trapped</polrule>
      </mnmchemgroup>
      <mnmchemgroup>
        <name>Lateral SH2</name>
        <pka>8.3</pka>
        <acidcharged>FALSE</acidcharged>
        <polrule>never_trapped</polrule>
      </mnmchemgroup>
    </monomer>
\endcode

Returns the count of rules that were accounted for, or -1 if none was.
*/
int
PkaPhPi::accountMonomerModif(const Monomer &monomer,
                             ChemicalGroup &chemical_group)
{
  QString modifName;
  ChemicalGroupRuleSPtr rule_sp = nullptr;

  int count = 0;

  // For each modification in the monomer, make the accounting work.

  std::size_t chem_group_rule_index = 0;

  for(const ModifSPtr &modif_sp : monomer.getModifsCstRef())
    {
      // Get the name of the modification of the monomer(if any) and get
      // the rule dealing with that monomer modification(if any).

      modifName = modif_sp->getName();

      rule_sp = chemical_group.findRuleByEntityAndName(
        "MONOMER_MODIF", modifName, chem_group_rule_index);

      if(modifName.isEmpty())
        {
          // The monomer does not seem to be modified. However, we still
          // have to make sure that the chemgroup that we were parsing is
          // actually a chemgroup suitable for a modif.  If this chemgroup
          // was actually suitable for a monomer modif, but it is not
          // effectively modified, that means that we have to calculate
          // the charge for the non-modified chemgroup...

          if(rule_sp != nullptr)
            {
              double charge = calculateChargeRatio(
                chemical_group.getPka(), chemical_group.isAcidCharged());
              if(charge < 0)
                m_negativeCharges += charge;
              else if(charge > 0)
                m_positiveCharges += charge;

              return ++count;
            }
          else
            {
              // The current monomer was NOT modified, and the chemgroup
              // was NOT eligible for a monomer modification. This means
              // that we do not have to process it, and we return -1 so
              // that the caller function knows we did not do anything and
              // that this chemgroup should continue to undergo analysis
              // without skipping it.

              return -1;
            }
        }
      // End of
      // if (modifName.isEmpty())

      if(rule_sp == nullptr)
        {
          // This chemgroup was not "designed" to receive any
          // modification, so we have nothing to do with it, and we return
          // -1 to let the caller know that its processing should be
          // continued in the caller's function space.

          return -1;
        }

      // At this point, we know that the chemgroup we are analyzing is
      // eligible for a modification and that we have a rule for it. Let's
      // continue the analysis:

      // Apparently, a rule object has member data matching the ones we
      // were looking for. At this point we should know what action to
      // take for this chemgroup.

      if(rule_sp->getFate() == Enums::ChemicalGroupFate::LOST)
        {
          // We do not use the current chemical group 'group' because the
          // monomer modification has abolished it.
        }
      else if(rule_sp->getFate() == Enums::ChemicalGroupFate::PRESERVED)
        {
          double charge = calculateChargeRatio(chemical_group.getPka(),
                                               chemical_group.isAcidCharged());
          if(charge < 0)
            m_negativeCharges += charge;
          else if(charge > 0)
            m_positiveCharges += charge;

          return ++count;
        }
      else
        qFatal() << "Programming error.";

      // Whatever we should do with this monomer's chemgroup, we should
      // take into account the modification itself that might have brought
      // chemgroup(s) worth calculating their intrinsic charges!

      // Find a modif object in the local list of modif objects, that has
      // the same name as the modification with which the monomer is
      // modified. We'll see what chemgroup(s) this modification brings to
      // the polymer sequence.

      std::vector<ModifCstSPtr>::const_iterator modif_monomer_iterator_cst =
        std::find_if(m_modifs.cbegin(),
                     m_modifs.cend(),
                     [modifName](const ModifCstSPtr &modif_csp) {
                       return modif_csp->getName() == modifName;
                     });

      if(modif_monomer_iterator_cst == m_modifs.cend())
        {
          //       qDebug() << __FILE__ << __LINE__
          // 		<< "Information: following modif not in local list:"
          // 		<< modifName;

          return count;
        }

      for(int jter = 0; jter < (*modif_monomer_iterator_cst)->propList().size();
          ++jter)
        {
          Prop *prop = (*modif_monomer_iterator_cst)->propList().at(jter);

          if(prop->name() != "CHEMICAL_GROUP")
            continue;

          // Get the chemical group out of the property.

          const ChemicalGroup *chemicalGroup =
            static_cast<const ChemicalGroup *>(prop->data());

          double charge = calculateChargeRatio(chemicalGroup->getPka(),
                                               chemicalGroup->isAcidCharged());
          if(charge < 0)
            m_negativeCharges += charge;
          else if(charge > 0)
            m_positiveCharges += charge;

          ++count;
        }
    }

  return count;
}

} // namespace libXpertMassCore
} // namespace MsXpS
