/* BEGIN software license
 *
 * msXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright(C) 2009,...,2018 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 <massxpert/nongui/PkaPhPi.hpp>


namespace msXpSmassXpert
{

PkaPhPi::PkaPhPi(Polymer &polymer,
                 CalcOptions &calcOptions,
                 QList<Monomer *> *monomerList,
                 QList<Modif *> *modifList)
  : m_polymer(polymer),
    m_calcOptions(calcOptions),
    mpa_monomerList(monomerList),
    mpa_modifList(modifList)
{
  m_ph              = 7;
  m_pi              = 7;
  m_positiveCharges = 0;
  m_negativeCharges = 0;

  mp_aborted  = 0;
  mp_progress = 0;
}


PkaPhPi::~PkaPhPi()
{
  if(mpa_monomerList)
    {
      while(!mpa_monomerList->isEmpty())
        delete mpa_monomerList->takeFirst();

      delete mpa_monomerList;
      mpa_monomerList = 0;
    }

  if(mpa_modifList)
    {
      while(!mpa_modifList->isEmpty())
        delete mpa_modifList->takeFirst();

      delete mpa_modifList;

      mpa_modifList = 0;
    }
}


void
PkaPhPi::setPh(double ph)
{
  Q_ASSERT(ph > 0 && ph < 14);

  m_ph = ph;
}


double
PkaPhPi::ph()
{
  return m_ph;
}


double
PkaPhPi::pi()
{
  return m_pi;
}


double
PkaPhPi::positiveCharges()
{
  return m_positiveCharges;
}


double
PkaPhPi::negativeCharges()
{
  return m_negativeCharges;
}


void
PkaPhPi::setCalcOptions(const CalcOptions &calcOptions)
{
  m_calcOptions = calcOptions;
}


void
PkaPhPi::setMonomerList(QList<Monomer *> *monomerList)
{
  Q_ASSERT(monomerList);

  mpa_monomerList = monomerList;
}


void
PkaPhPi::setModifList(QList<Modif *> *modifList)
{
  Q_ASSERT(modifList);

  mpa_modifList = modifList;
}


int
PkaPhPi::calculateCharges()
{
  int processedChemicalGroups = 0;

  m_positiveCharges = 0;
  m_negativeCharges = 0;

  // We of course need monomers ! Instead, we may not need modifs.
  if(!mpa_monomerList)
    return -1;

  int polymerSize = m_polymer.size();

  // The general scheme is :

  // Get the list of the coordinates of the different 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 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.

  const CoordinateList &coordinateList = m_calcOptions.coordinateList();

  for(int iter = 0; iter < coordinateList.size(); ++iter)
    {
      Coordinates *coordinates = coordinateList.at(iter);

      int startIndex = coordinates->start();
      int endIndex   = coordinates->end();

      bool leftMostCoordinates =
        coordinateList.isLeftMostCoordinates(coordinates);
      bool rightMostCoordinates =
        coordinateList.isRightMostCoordinates(coordinates);

      for(int jter = startIndex; jter < endIndex + 1; ++jter)
        {
          const Monomer *seqMonomer = m_polymer.at(jter);

          // 	    qDebug() << __FILE__ << __LINE__
          // 		      << "-- Monomer:" << seqMonomer->name()
          // 		      << "position:" << jter + 1;

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

          int index =
            Monomer::isCodeInList(seqMonomer->code(), *mpa_monomerList);
          if(index == -1)
            return -1;

          const Monomer *monomer = mpa_monomerList->at(index);
          Q_ASSERT(monomer);

          // 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->propList().size(); ++kter)
            {
              Prop *prop = monomer->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(chemicalGroup->polRule() & CHEMGROUP_LEFT_TRAPPED)
                {
                  // 		    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(!leftMostCoordinates)
                        {
                          // The current Coordinates is not the
                          // left-most Coordinates in the
                          // CoordinateList, thus we cannot consider
                          // it to be the "left end coordinates" of
                          // the CoordinateList. Just continue
                          // without exploring any more.
                          continue;
                        }
                      if(jter == startIndex)
                        {
                          // The current monomer is the first
                          // monomer of Coordinates. We only take
                          // into account the chemical group if each
                          // Coordinates is to be considered an
                          // oligomer.

                          if(m_calcOptions.selectionType() !=
                             SELECTION_TYPE_OLIGOMERS)
                            continue;
                        }
                    }
                }

              if(chemicalGroup->polRule() & CHEMGROUP_RIGHT_TRAPPED)
                {
                  // 		    qDebug() << __FILE__ << __LINE__
                  // 			      << "... that is CHEMGROUP_RIGHT_TRAPPED";

                  // See explanations above.

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

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

                          if(m_calcOptions.selectionType() !=
                             SELECTION_TYPE_OLIGOMERS)
                            continue;
                        }
                    }
                }

              if(iter == 0 && m_calcOptions.polymerEntities() &
                                POLYMER_CHEMENT_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(
                    POLYMER_CHEMENT_LEFT_END_MODIF, *chemicalGroup);
                  if(ret >= 0)
                    {
                      // 			qDebug() << __FILE__ << __LINE__
                      // 				  << "Accounted for left end modif.";

                      processedChemicalGroups += ret;
                      continue;
                    }
                }

              if(iter == polymerSize - 1 && m_calcOptions.polymerEntities() &
                                              POLYMER_CHEMENT_RIGHT_END_MODIF)
                {
                  int ret = accountPolymerEndModif(
                    POLYMER_CHEMENT_RIGHT_END_MODIF, *chemicalGroup);
                  if(ret >= 0)
                    {
                      // 			qDebug() << __FILE__ << __LINE__
                      // 				  << "Accounted for right end modif.";

                      processedChemicalGroups += ret;
                      continue;
                    }
                }

              if(m_calcOptions.monomerEntities() & MONOMER_CHEMENT_MODIF &&
                 seqMonomer->isModified())
                {
                  int ret = accountMonomerModif(*seqMonomer, *chemicalGroup);
                  if(ret >= 0)
                    {
                      // 			qDebug() << __FILE__ << __LINE__
                      // 				  << "Accounted for monomer modif.";

                      processedChemicalGroups += ret;
                      continue;
                    }
                }

              double charge = calculateChargeRatio(
                chemicalGroup->pka(), 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 = startIndex; jter < endIndex + 1; ++jter)

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

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

  return processedChemicalGroups;
}


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(!mpa_monomerList)
    {
      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;
}


double
PkaPhPi::calculateChargeRatio(double pka, bool acidCharged)
{
  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(acidCharged)
        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(acidCharged)
        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(acidCharged)
        return (aOverAh / 2);
      else
        return (-aOverAh / 2);
    }
  else
    Q_ASSERT(0);

  return 0;
}


int
PkaPhPi::accountPolymerEndModif(int endModif, const ChemicalGroup &group)
{
  QString modifName;
  ChemicalGroupRule *rule = 0;

  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).

  if(endModif == POLYMER_CHEMENT_LEFT_END_MODIF)
    {
      modifName = m_polymer.leftEndModif().name();

      rule = group.findRule("LE_PLM_MODIF", modifName);

      // Remember a chemical group is defined like this:

      //       <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>
    }
  else if(endModif == POLYMER_CHEMENT_RIGHT_END_MODIF)
    {
      modifName = m_polymer.rightEndModif().name();

      rule = group.findRule("RE_PLM_MODIF", modifName);
    }
  else
    Q_ASSERT(0);


  // 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(rule)
        {
          double charge =
            calculateChargeRatio(group.pka(), 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(!rule)
    {
      // 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(rule->outcome() == CHEMGROUP_RULE_LOST)
    {
      // We do not use the current chemical group 'group' because the
      // polymer end's modification has abolished it.
    }
  else if(rule->outcome() == CHEMGROUP_RULE_PRESERVED)
    {
      double charge = calculateChargeRatio(group.pka(), group.isAcidCharged());
      if(charge < 0)
        m_negativeCharges += charge;
      else if(charge > 0)
        m_positiveCharges += charge;

      return ++count;
    }
  else
    Q_ASSERT(0);

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

  int index = Modif::isNameInList(modifName, *mpa_modifList);

  if(index == -1)
    {
      //       qDebug() << __FILE__ << __LINE__
      // 		<< "Information: following modif not in local list:"
      // 		<< modifName;

      return count;
    }

  const Modif *modif = mpa_modifList->at(index);
  Q_ASSERT(modif);

  for(int jter = 0; jter < modif->propList().size(); ++jter)
    {
      Prop *prop = modif->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->pka(),
                                           chemicalGroup->isAcidCharged());
      if(charge < 0)
        m_negativeCharges += charge;
      else if(charge > 0)
        m_positiveCharges += charge;

      ++count;
    }

  return count;
}


int
PkaPhPi::accountMonomerModif(const Monomer &monomer, ChemicalGroup &group)
{
  QString modifName;
  ChemicalGroupRule *rule = 0;

  int count = 0;

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

  Q_ASSERT(mpa_modifList);
  Q_ASSERT(mpa_modifList->size());

  for(int iter = 0; iter < monomer.modifList()->size(); ++iter)
    {
      Modif *iterModif = monomer.modifList()->at(iter);

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

      modifName = iterModif->name();

      rule = group.findRule("MONOMER_MODIF", modifName);

      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)
            {
              double charge =
                calculateChargeRatio(group.pka(), 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)
        {
          // 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->outcome() == CHEMGROUP_RULE_LOST)
        {
          // We do not use the current chemical group 'group' because the
          // monomer modification has abolished it.
        }
      else if(rule->outcome() == CHEMGROUP_RULE_PRESERVED)
        {
          double charge =
            calculateChargeRatio(group.pka(), group.isAcidCharged());
          if(charge < 0)
            m_negativeCharges += charge;
          else if(charge > 0)
            m_positiveCharges += charge;

          return ++count;
        }
      else
        Q_ASSERT(0);

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

      int index = Modif::isNameInList(modifName, *mpa_modifList);

      if(index == -1)
        {
          //       qDebug() << __FILE__ << __LINE__
          // 		<< "Information: following modif not in local list:"
          // 		<< modifName;

          return count;
        }

      Modif *modif = mpa_modifList->at(index);
      Q_ASSERT(modif);

      for(int jter = 0; jter < modif->propList().size(); ++jter)
        {
          Prop *prop = modif->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->pka(),
                                               chemicalGroup->isAcidCharged());
          if(charge < 0)
            m_negativeCharges += charge;
          else if(charge > 0)
            m_positiveCharges += charge;

          ++count;
        }
    }

  return count;
}

} // namespace msXpSmassXpert
