/* BEGIN software license
 *
 * msXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright (C) 2009--2020 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
 */


/////////////////////// Qt includes
#include <QChar>
#include <QString>


/////////////////////// Local includes
#include "Formula.hpp"


namespace msxps
{
namespace minexpert
{


//! Construct a formula initialized with a formula string
/*!

  The formula is initialized using \p formula. Upon construction of
  the formula, no parsing occurs.

  \p formula gets simply copied into the \c m_formula.

  \param formula string representing a formula or an actionformula. Defaults
  to a null string.

*/
Formula::Formula(const QString &formula) : m_formula{formula}
{
}


//! Construct \c this Formula as a copy of \p other.
/*!

  The copying is deep, with all the member of \p other being copied in \c
  this Formula.

  \param other formula to be used as a mold.

*/
Formula::Formula(const Formula &other)
  : m_formula{other.m_formula},
    m_plusFormula{other.m_plusFormula},
    m_minusFormula{other.m_minusFormula}
{
  AtomCount *atomCount = Q_NULLPTR;

  for(int iter = 0; iter < other.m_atomCountList.size(); ++iter)
    {
      atomCount = new AtomCount(*other.m_atomCountList.at(iter));

      m_atomCountList.append(atomCount);
    }
}


//! Destroy the formula.
/*!

  The heap-allocated AtomCount instances are deleted.

*/
Formula::~Formula()
{
  while(!m_atomCountList.isEmpty())
    delete m_atomCountList.takeFirst();
}


//! Return a const reference to the list of AtomCount objects.
/*!

  \return A const reference to the list of atom count objects (m_atomCountList).

*/
const QList<AtomCount *> &
Formula::atomCountList() const
{
  return m_atomCountList;
}


//! Create a new formula initialized using \c this.
/*!

  The copy is deep, with all the members of \c this being copied into the new
  Formula instance.

  \return The new Formula, which should be deleted when no more in use.

*/
Formula *
Formula::clone() const
{
  Formula *other = new Formula(*this);

  return other;
}

//! Modify \p other to be identical to \c this.
/*!

  The copying is deep, with all the members of \c this being copied into \p
  other.

  \param other Formula instance being modified.

*/
void
Formula::clone(Formula *other) const
{
  if(other == this)
    return;

  AtomCount *atomCount = 0;

  Q_ASSERT(other);

  other->m_formula      = m_formula;
  other->m_plusFormula  = m_plusFormula;
  other->m_minusFormula = m_minusFormula;

  while(!other->m_atomCountList.isEmpty())
    delete other->m_atomCountList.takeFirst();

  for(int iter = 0; iter < m_atomCountList.size(); ++iter)
    {
      atomCount = new AtomCount();

      m_atomCountList.at(iter)->clone(atomCount);

      other->m_atomCountList.append(atomCount);
    }
}


//! Modify \c this to be identical to \p other.
/*!

  The copying is deep, as all the member data of \p other are copied into \c
  this Formula.

  \param other Formula to be used as a mold.

*/
void
Formula::mold(const Formula &other)
{
  if(&other == this)
    return;

  AtomCount *atomCount = 0;

  m_formula      = other.m_formula;
  m_plusFormula  = other.m_plusFormula;
  m_minusFormula = other.m_minusFormula;

  while(!m_atomCountList.isEmpty())
    delete m_atomCountList.takeFirst();

  for(int iter = 0; iter < other.m_atomCountList.size(); ++iter)
    {
      atomCount = new AtomCount();

      atomCount->mold(*other.m_atomCountList.at(iter));

      m_atomCountList.append(atomCount);
    }
}


//! Assign \p other to \p this Formula.
/*!

  The copying is deep, as all the member data for \p other are copied into \c
  this Formula.

  \param other formula.

  \return a reference to \c this Formula.

*/
Formula &
Formula::operator=(const Formula &other)
{
  if(&other != this)
    mold(other);

  return *this;
}


//! Set the actionformula.
/*!

  The  \p formula is copied to \c this \c m_formula.  No other processing is
  performed.

  \param formula formula or actionformula initializer.
  */
void
Formula::setFormula(const QString &formula)
{
  m_formula = formula;
}


//! Set the formula or the actionformula.
/*!

  The \c m_formula member of the argument \p formula is copied to \c this \c
  m_formula.  No other processing is performed.

  \param formula \Formula from which to copy the \c m_formula.
  */
void
Formula::setFormula(const Formula &formula)
{
  m_formula = formula.m_formula;
}


//! Return the formula as a string.
/*!

  \return the formula as a string.

*/
QString
Formula::text() const
{
  return m_formula;
}


void
Formula::setForceCountIndex(bool forceCountIndex)
{
  m_forceCountIndex = forceCountIndex;
}


//! Clear all the formula member data.
/*!

  The AtomCount instances are freed also.

*/
void
Formula::clear()
{
  m_formula.clear();
  m_plusFormula.clear();
  m_minusFormula.clear();

  while(!m_atomCountList.isEmpty())
    delete m_atomCountList.takeFirst();
}


//! Set the \c m_plusFormula component.
/*!

  \param formula formula initializer.

*/
void
Formula::setPlusFormula(const QString &formula)
{
  m_plusFormula = formula;
}


//! Return the \c m_plusFormula component.
/*!

  \return the \c plusFormula component as a string.

*/
const QString &
Formula::plusFormula() const
{
  return m_plusFormula;
}


//! Set the \c m_minusFormula component.
/*!

  \param formula formula initializer.

*/
void
Formula::setMinusFormula(const QString &formula)
{
  m_minusFormula = formula;
}


//! Return the \c m_minusFormula component.
/*!

  \return the \c minusFormula component as a string.

*/
const QString &
Formula::minusFormula() const
{
  return m_minusFormula;
}


//! Test equality.
/*!

  The test only involves to the formula, not the minus-/plus-formulas nor the
  list of AtomCount instances.

  \param other formula to be compared with \c this.

  \return true if the formulas are identical, false otherwise.

*/
bool
Formula::operator==(const Formula &other) const
{
  return (m_formula == other.m_formula);
}


//! Test inequality.
/*!

  The test only involves to the formula, not the minus-/plus-formulas nor the
  list of AtomCount instances.

  \param other formula to be compared with \c this.

  \return true if the formulas differ, false otherwise.

*/
bool
Formula::operator!=(const Formula &other) const
{
  return (m_formula != other.m_formula);
}


//! Tell the actions found in the formula.
/*!

  Following analysis of the \p formula argument, this function will
  be able to tell if the formula contains only '+'-associated elements
  or also '-'-associated elements.

  If a formula contains no sign at all, then it is considered to contain only
  '+'-associated member(s). If no '+' sign is found and one member if found
  associated to a '-', then the minus action prevails. In other words, this
  function check if '-' members are found.

  This function is used to quickly have an indication if the
  splitParts() function is to be run or if it is not necessary.

  \param formula formula to report the actions about.

  \return '+' if no '-' action was found, '-' otherwise.

  \sa splitParts()
  */
QChar
Formula::actions(const QString &formula)
{
  int minusCount = formula.count('-', Qt::CaseInsensitive);

  return (minusCount == 0 ? '+' : '-');
}


//! Tell the actions found in \c this Formula instance.
/*!

  Following analysis of \c m_formula, this function will be able to tell if
  the formula contains only '+'-associated elements or also '-'-associated
  elements.

  If a formula contains no sign at all, then it is considered to contain only
  '+'-associated member(s). If no '+' sign is found and one member is found
  associated to a '-', then the minus action prevails. In other words, this
  function check if '-' members are found.

  This function is used to quickly have an indication if the
  splitParts() function is to be run or if it is not necessary.

  \param formula formula to report the actions about.

  \return '+' if no '-' action was found, '-' otherwise.

  \sa splitParts()
  */
QChar
Formula::actions() const
{
  return actions(m_formula);
}

//! Remove the title from the \c m_formula actionformula.
/*!

  The <em>title</em> of a formula is the documentation string, enclosed in
  double quotes, that is located in front of the actual chemical
  actionformula.  This function removes that <em>title</em> element from the
  \c m_formula actionformula. The \e title substring of the actionformula is
  removed using a QRegExp.

  \return the number of removed characters.

*/
int
Formula::removeTitle()
{
  int length = m_formula.length();

  m_formula.remove(QRegExp("\".*\""));

  return (length - m_formula.length());
}


//! Remove the space characters from the \c m_formula actionformula.
/*!

  Spaces can be placed anywhere in formula for more readability. However, it
  might be required that these character spaces be removed. This function does
  just this, using a QRegExp.

  \return the number of removed characters.

*/
int
Formula::removeSpaces()
{
  int length = m_formula.length();

  // We want to remove all the possibly-existing spaces.

  m_formula.remove(QRegExp("\\s+"));

  // Return the number of removed characters.
  return (length - m_formula.length());
}


//! Split the formula according to its plus-/minus- actions.
/*!

  Parses the \c m_formula actionformula and separates all the minus components
  of that actionformula from all the plus components. The different components
  are set to their corresponding formula (\c m_minusFormula and \c
  m_plusFormula).

  At the end of the split work, each sub-formula (plus- and/or minus-)
  is actually parsed for validity, using the reference atom list.

  If \p times is not 1, then the accounting of the plus/minus formulas is
  compounded by this factor.

  \param refList List of reference atoms.

  \param times Number of times the formula has to be accounted
  for. Defaults to 1.

  \param store Indicates if AtomCount objects created during the
  parsing of the sub-formulas generated by the split of the formula
  have to be stored, or not. Defaults to false.

  \param reset Indicates if the list of AtomCount objects has to be reset
  before the splitParts work. This parameter may be useful if the caller needs
  to "accumulate" the accounting of the formula (in which case it should be
  set to \c false). Defaults to false.

  \return FormulaSplitResult::FORMULA_SPLIT_FAIL if the splitting failed,
  FormulaSplitResult::FORMULA_SPLIT_PLUS if the components of the formula are
  all of type plus, FormulaSplitResult::FORMULA_SPLIT_MINUS if all the
  components of the formula are of type minus. The result value can be an
  OR'ing of FormulaSplitResult::FORMULA_SPLIT_PLUS and
  FormulaSplitResult::FORMULA_SPLIT_MINUS if both plus and minus components
  were found in the \c m_formula actionformula.

*/
int
Formula::splitParts(const QList<Atom *> &refList,
                    int times,
                    bool store,
                    bool reset)
{
  QChar curChar;
  QString tempFormula;

  int result = 0;

  bool wasParsingFormula = false;
  bool shouldBeFormula   = false;
  bool wasMinusSign      = false;

  if(refList.isEmpty())
    qFatal("Programming error.");

  // We are asked to put all the '+' components of the formula
  // into corresponding formula and the same for the '-' components.

  m_plusFormula.clear();
  m_minusFormula.clear();

  // Because the formula that we are analyzing might contain a title
  // and spaces , we first remove these. But make a local copy of
  // the member datum.

  QString formula = m_formula;

  // qDebug() << "splitParts before working:"
  // << "m_formula:" << m_formula
  // << "text" << Formula::text();

  // One formula can be like this:

  // "Decomposed adenine" C5H4N5 +H

  // The "Decomposed adenine" is the title
  // The C5H4N5 +H is the formula.

  formula.remove(QRegExp("\".*\""));

  // We want to remove all the possibly-existing spaces.

  formula.remove(QRegExp("\\s+"));

  // If the formula does not contain any '-' character, then we
  // can approximate that all the formula is a '+' formula, that is a
  // plusFormula:

  if(actions() == '+')
    {
      m_plusFormula.append(formula);

      // At this point we want to make sure that we have a correct
      // formula. Remove all the occurrences of the '+' sign.
      m_plusFormula.replace(QString("+"), QString(""));

      if(m_plusFormula.length() > 0)
        {
          // qDebug() << "splitParts: with m_plusFormula:"
          // << m_plusFormula;

          if(!parse(refList, m_plusFormula, times, store, reset))
            return FORMULA_SPLIT_FAIL;
          else
            return FORMULA_SPLIT_PLUS;
        }
    }
  // End of
  // if(actions() == '+')

  // At this point, we truly have to iterate in the formula...

  for(int iter = 0; iter < formula.length(); ++iter)
    {
      curChar = formula.at(iter);
      // qDebug() << "curChar:" << curChar;

      if(curChar == '+' || curChar == '-')
        {
          if(shouldBeFormula)
            return FORMULA_SPLIT_FAIL;

          if(wasParsingFormula)
            {
              // We were parsing a formula, wich means that we are
              // ending that formula now, by starting another one. For
              // example, if we had "-CH3+COOH" we would typically be
              // at the '+' after having parsed -CH3. So we now have
              // to account for that latter formula.

              if(wasMinusSign)
                m_minusFormula.append(tempFormula);
              else
                m_plusFormula.append(tempFormula);

              // Reinit the tempFormula for next round.
              tempFormula.clear();

              // Now set proper bool values for next round.
              shouldBeFormula = true;
              wasMinusSign    = (curChar == '-' ? true : false);

              continue;
            }
          else
            {
              wasMinusSign    = (curChar == '-' ? true : false);
              shouldBeFormula = true;

              continue;
            }
        }
      else
        {
          // We are parsing either a digit or an alphabetical
          // character : we just append it to the tempFormula:
          tempFormula.append(curChar);

          wasParsingFormula = true;

          // We do not necessarily have to expect another formula
          // component at next round, admitting we were on the
          // nitrogen atom of CH3CN:
          shouldBeFormula = false;

          continue;
        }
    } // End for (int iter = 0 ; iter < formula.length() ; ++iter)

  // At this point the loop was finished so we might have something
  // interesting cooking:

  if(wasParsingFormula && tempFormula.length() > 0)
    {
      if(wasMinusSign)
        m_minusFormula.append(tempFormula);
      else
        m_plusFormula.append(tempFormula);
    }

  // At this point we want to make sure that we have a correct
  // formula. First reset the atomcount stuff if required.

  if(reset)
    {
      while(!m_atomCountList.isEmpty())
        delete m_atomCountList.takeFirst();
    }

  // qDebug() << "splitParts:"
  // << "right after splitting:"
  // << "m_formula:" << m_formula
  // << "text" << text();


  // Now that we have reset (if required) the atomCountList, we need not
  // and we must not reset during the parsing below, otherwise if we
  // have -H+H3PO4, then we'll compute +H3PO4 first, then we compute
  // -H with reset to true : the +H3PO4 component is destroyed!

  if(m_plusFormula.length() > 0)
    {
      bool res = parse(refList, m_plusFormula, times, store, false /* reset */);

      // qDebug() << "splitParts:"
      // << "right after parse of m_plusFormula:"
      // << m_plusFormula
      // << "m_formula:" << m_formula
      // << "text" << text();

      if(!res)
        return FORMULA_SPLIT_FAIL;
      else
        result = FORMULA_SPLIT_PLUS;
    }


  if(m_minusFormula.length() > 0)
    {
      bool res =
        parse(refList, m_minusFormula, -times, store, false /* reset */);

      // qDebug() << "splitParts:"
      // << "right after parse of m_minusFormula:"
      // << m_minusFormula
      // << "m_formula:" << m_formula
      // << "text" << text();

      if(!res)
        return FORMULA_SPLIT_FAIL;
      else
        result |= FORMULA_SPLIT_MINUS;
    }


  // qDebug() << formula.toAscii() << "-->"
  // << "(+)" << m_plusFormula.toAscii()
  // << "(-)" << m_minusFormula.toAscii();

  return result;
}


//! Parse the \p formula using the reference atom list.
/*!

  Upon parsing of the formula, a list of AtomCount objects are created in
  order to be able to account for the mass of the formula.

  If \p times is greater than one, then the mass contribution of the formula
  is compounded by this factor.

  \param refList List of reference atoms.

  \param formula Formula to parse.

  \param times Number of times that the formula should be accounted
  for. Default value is 1.

  \param store Indicates if AtomCount objects created during the
  parsing of the formula have to be stored, or not. Default value is
  false.

  \param reset Indicates if AtomCount objects created during a previous
  parsing of the formula have to be destroyed before doing another parsing.
  This parameter is interesting if the caller needs to "accumulate" the
  accounting of the formula. Default value is false.

  \return true if parsing succeeded, false otherwise.

*/
bool
Formula::parse(const QList<Atom *> &refList,
               const QString &formula,
               int times,
               bool store,
               bool reset)
{
  QChar curChar;
  QString parsedCount;
  QString parsedSymbol;
  AtomCount atomCount;

  bool wasDigit = false;
  bool gotUpper = false;

  Q_ASSERT(refList.size());

  // The formula member is a QString that should hold the formula
  // according to this typical schema: "H2O"(water). That means we
  // only want letters(Upper and lower case and number).

  // The member atomCountList might be reset before starting, or if
  // !reset, then the new atom counts are added to the ones
  // preexisting.

  // The formula should thus not be empty, otherwise there is nothing
  // to do. But it is not an error that the formula be empty.
  if(formula.length() == 0)
    return true;

  if(!checkSyntax(formula))
    return false;

  // Also, the first character of the formula should be an Uppercase
  // letter. If not, logically, the formula is incorrect.
  if(formula.at(0).category() != QChar::Letter_Uppercase)
    return false;

  if(reset)
    {
      // We first want to iterate in the atomCountList and make sure
      // we remove all items from it.

      while(!m_atomCountList.isEmpty())
        delete m_atomCountList.takeFirst();
    }

  // And now finally start the real parsing stuff.

  for(int iter = 0; iter < formula.length(); ++iter)
    {
      curChar = formula.at(iter);

      if(curChar.category() == QChar::Number_DecimalDigit)
        {
          // We are parsing a digit.

          parsedCount.append(curChar);

          wasDigit = true;

          continue;
        }
      else if(curChar.category() == QChar::Letter_Lowercase)
        {
          // Current character is lowercase, which means we are inside
          // of an atom symbol, such as Ca(the 'a') or Nob(either
          // 'o' or 'b'). Thus, gotUpper should be true !

          if(!gotUpper)
            return false;

          // Make use of the parsed numerical character.
          parsedSymbol.append(curChar);

          // Let the people know that we have parsed a lowercase char
          // and not a digit.
          wasDigit = false;
        }
      else if(curChar.category() == QChar::Letter_Uppercase)
        {
          // Current character is uppercase, which means that we are
          // at the beginning of an atom symbol. Check if there was a
          // symbol being parsed before this one.

          if(parsedSymbol.isEmpty())
            {
              // Start new parsing round.
              parsedSymbol.append(curChar);

              gotUpper = true;
              wasDigit = false;
              continue;
            }

          // There was a symbol being parsed. Fill-in the atomCount
          // object.
          atomCount.setSymbol(parsedSymbol);

          // Now we can prepare the field for the next one.
          parsedSymbol.clear();
          parsedSymbol.append(curChar);

          // Before going on, check if the symbol is correct.
          if(atomCount.symbolIndex(refList) == -1)
            return false;

          // If there was a count being parsed, we have to take it
          // into account.
          if(wasDigit)
            {
              // And now we have to convert the string representation
              // of the atom count for that atom to int. In fact, we
              // have to be able to know that water H2O has TWO
              // hydrogen atoms in it.
              bool isok = true;
              atomCount.setCount(parsedCount.toInt(&isok, 10));

              if(atomCount.count() == 0 && !isok)
                // The atom counts for nothing ! Or was there
                // an error in the conversion ?
                return false;

              // But we remember that we have to take into account the
              // times parameter.

              atomCount.setCount(atomCount.count() * times);

              // Clear parsedCount for next count parsing round.
              parsedCount.clear();
            }
          else
            atomCount.setCount(1 * times);

          // We can now make sure that the atom gets represented
          // in the formula.atomCountList list of
          // AtomCount*. But for this we use a function that
          // will make sure there is not already the same atom
          // symbol in that List, so as not to duplicate the items
          // accounting for a single atom symbol.

          if(store)
            {
              accountInList(atomCount);

              // qDebug() << "accountInList:"
              // << atomCount.symbol() << atomCount.count();
            }

          // Let the people know what we got:

          wasDigit = false;
          gotUpper = true;
        }
      // end(curChar.category() == QChar::Letter_Uppercase)
    }
  // end for (int iter = 0 ; iter < formula.length() ; ++iter)

  // At this point we are at then end of the string, and we thus might
  // still have something cooking:

  // Thus we have to check that the last parsed atom
  // symbol is correct.

  atomCount.setSymbol(parsedSymbol);

  if(atomCount.symbolIndex(refList) == -1)
    return false;

  // And now we have to convert the string representation
  // of the atom count for that atom to int. In fact, we
  // have to be able to know that water H2O has TWO
  // hydrogen atoms in it.

  // If there was a count being parsed, we have to take it
  // into account.
  if(wasDigit)
    {
      // And now we have to convert the string representation
      // of the atom count for that atom to int. In fact, we
      // have to be able to know that water H2O has TWO
      // hydrogen atoms in it.
      bool isok = true;
      atomCount.setCount(parsedCount.toInt(&isok, 10));

      if(atomCount.count() == 0 && !isok)
        // The atom counts for nothing ! Or was there
        // an error in the conversion ?
        return false;

      // But we remember that we have to take into account the
      // times parameter.

      atomCount.setCount(atomCount.count() * times);
    }
  else
    atomCount.setCount(1 * times);

  // Finally, if asked by the caller, we can account for
  // this atom symbol/count also !

  if(store)
    {
      accountInList(atomCount);

      // qDebug() << "accountInList:"
      // << atomCount.symbol() << atomCount.count();
    }

  return true;
}


//! Account for the \p atomCount in the member \c m_atomCountList.
/*!

  Search for an AtomCount instance in \c m_atomCountList that has the same
  symbol as the symbol of \p atomCount.

  If an AtomCount is found in	\c m_atomCountLIst, increment its count value by
  the count value in \p atomCount. The count value of \p atomCount can be
  either positive or negative. After this operation, if the count of \this
  m_atomCountList AtomCount instance is 0, then remove the AtomCount from the
  list.

  If no AtomCount is found in	\c m_atomCountLIst, create a new copy of \p
  atomCount and append it to \c m_atomCountList.

  \return the new count value of the AtomCount instance in \c m_atomCountList.

*/
int
Formula::accountInList(const AtomCount &atomCount, int times)
{
  int count    = atomCount.count() * times;
  int newCount = 0;
  bool found   = false;

  for(int iter = 0; iter < m_atomCountList.size(); ++iter)
    {
      if(m_atomCountList.at(iter)->symbol() == atomCount.symbol())
        {
          // qDebug() << "accountInList:"
          // << "same symbol:" << atomCount.symbol();

          found = true;

          // Same atom found. If the result of accounting is that
          // the count is 0, then remove the atomCount instance
          // alltogether. For example, admitting that there was 1
          // Carbon atom in the list, and we account -1 Carbon, the
          // result is that there is no Carbon atom remaining in the
          // list. The call below would return 0. In that case
          // remove an atomCount item of which the count is 0.

          newCount = m_atomCountList.at(iter)->account(count);

          if(!newCount)
            {
              delete m_atomCountList.takeAt(iter);

              // qDebug() << "accountInList:"
              // "deleted atomCount instance "
              // "because count reached 0";
            }
          else
            {
              // qDebug() << "accountInList:"
              // << "new count:"
              // << m_atomCountList.at(iter)->count();
            }

          // We have found an atomCount instance by the same symbol,
          // there should not be twice the same symbol in a formula,
          // thus we can break the loop.
          break;
        }

      continue;
    }
  // End of
  // for(int iter = 0; iter < m_atomCountList.size(); ++iter)

  if(!found)
    {
      // AtomCount not found locally. Thus make a copy and append to
      // *this m_atomCountList..

      AtomCount *newAtomCount = new AtomCount(atomCount);

      m_atomCountList.append(newAtomCount);

      newCount = count;
    }
  else
    {
      // One AtomCount was found locally, newCount contains the new
      // count for that found AtomCount.
    }

  // Update what's the text of the formula to represent what is in
  // atomCount list.
  m_formula = elementalComposition();

  return newCount;
}


//! Parse, validate and account for the \p text formula.
/*!

  The \p text is used to create a temporary Formula and to validate it against
  \p atomRefList. If the validation is successful, that temporary formula has
  an updated  \c m_atomCountList. Make a local copy of that \c m_atomCountList
  and for each AtomCount instance in it, make \c this Formula account it using
  the accountInList(const AtomCount &, int = 1) function.

  Finally use elementalComposition() to update the \m_formula so that it
  faithfully represents the contents of \c this Formula \c m_atomCountList
  member..

  \return the number of AtomCount instances that were accounted for after the
  temporary Formula validation or -1 if the temporary Formula failed to
  validate because \p text did not contain a valid actionformula.

*/
int
Formula::accountInList(const QString &text,
                       const QList<Atom *> &atomRefList,
                       int times)
{
  // We get a formula as an elemental composition and we want to
  // account for that formula in *this formula.

  // First off, validate the text.

  Formula formula(text);

  if(!formula.validate(atomRefList, true, true))
    return -1;

  // Now, for each AtomCount search one in the current formula.

  const QList<AtomCount *> &otherAtomCountList = formula.atomCountList();

  for(int iter = 0; iter < otherAtomCountList.size(); ++iter)
    accountInList(*otherAtomCountList.at(iter), times);

  // Update what's the text of the formula to represent what is in
  // atomCount list.
  m_formula = elementalComposition();

  return otherAtomCountList.size();
}


//! Check the syntax of the \p formula.
/*!

  The syntax of the \p formula is checked by verifying that the
  letters and ciphers in the formula are correctly placed. That is, we
  want that the ciphers appear after an atom symbol and not before
  it. We want that the atom symbol be made of one uppercase letter and
  that the following letters be lowercase.

  \attention This is a syntax check and not a true validation, as the formula
  can contain symbols that are syntactically valid but corresponding to atom
  definitions not available on the system. Indeed, there is no comparison of
  the symbols with those in an Atom reference list in this function. See the
  validate() function for a more in-depth check of the formula.

  \param formula the formula to check.

  \return true upon successful check, false otherwise.

  \sa validate().

*/
bool
Formula::checkSyntax(const QString &formula, bool forceCountIndex)
{
  // qDebug() << "with formula:" << formula;

  QChar curChar;

  bool gotUpper = false;
  bool wasSign  = false;
  bool wasDigit = false;

  // Because the formula that we are analyzing might contain a title
  // and spaces , we first remove these. But make a local copy of
  // the member datum.

  QString localFormula = formula;

  // One formula can be like this:

  // "Decomposed adenine" C5H4N5 +H

  // The "Decomposed adenine" is the title
  // The C5H4N5 +H is the formula.

  localFormula.remove(QRegExp("\".*\""));

  // We want to remove all the possibly-existing spaces.

  localFormula.remove(QRegExp("\\s+"));


  for(int iter = 0; iter < localFormula.length(); ++iter)
    {
      curChar = localFormula.at(iter);

      // qDebug() << "Current character:" << curChar;

      if(curChar.category() == QChar::Number_DecimalDigit)
        {
          // We are parsing a digit.

          // We may not have a digit after a +/- sign.
          if(wasSign)
            return false;

          wasSign  = false;
          wasDigit = true;

          continue;
        }
      else if(curChar.category() == QChar::Letter_Lowercase)
        {
          // Current character is lowercase, which means we are inside
          // of an atom symbol, such as Ca(the 'a') or Nob(either
          // 'o' or 'b'). Thus, gotUpper should be true !

          if(!gotUpper)
            return false;


          // We may not have a lowercase character after a +/- sign.
          if(wasSign)
            return false;

          // Let the people know that we have parsed a lowercase char
          // and not a digit.
          wasSign = false;

          wasDigit = false;
        }
      else if(curChar.category() == QChar::Letter_Uppercase)
        {
          // Current character is uppercase, which means that we are
          // at the beginning of an atom symbol.

          // There are two cases:
          // 1. We are starting for the very beginning of the formula, and
          // nothing came before this upper case character. That's fine.
          // 2. We had previously parsed a segment of the formula, and in this
          // case, we are closing a segment. If the parameter
          // obligatoryCountIndex is true, then we need to ensure that the
          // previous element had an associated number, even it the count
          // element is 1. This is required for the IsoSpec stuff in the gui
          // programs.

          if(iter > 0)
            {
              if(forceCountIndex)
                {
                  if(!wasDigit)
                    {
                      qDebug()
                        << "Returning false because upper case char was not"
                           "preceded by digit while not at the first char of "
                           "the formula";
                      return false;
                    }
                }
            }

          // Let the people know what we got:

          wasSign  = false;
          gotUpper = true;
          wasDigit = false;
        }
      else
        {
          if(curChar != '+' && curChar != '-')
            return false;
          else
            {
              // We may not have 2 +/- signs in a raw.
              if(wasSign)
                return false;
            }

          wasSign  = true;
          gotUpper = false;
          wasDigit = false;
        }
    }
  // end for (int iter = 0 ; iter < localFormula.length() ; ++iter)

  // Note that if we want an obligatory count index, then, at the end of the
  // formula, *compulsorily* we must have parsed a digit.

  if(forceCountIndex && !wasDigit)
    {
      qDebug()
        << "Returning false because the formula does not end with a digit.";
      return false;
    }

  // At this point we found no error condition.
  return true;
}


//! Check the syntax of \c this Formula \c m_formula actionformula member.
/*!

  The syntax of the \c m_formula is checked by verifying that the letters and
  ciphers in the formula are correctly placed. That is, we want that the
  ciphers appear after an atom symbol and not before it. We want that the atom
  symbol be made of one uppercase letter and that the following letters be
  lowercase.

  Note that the checking only concerns \c m_formula, and not the
  minus-/plus- formulas.

  \attention This is a syntax check and not a true validation, as the formula
  can contain symbols that are syntactically valid but corresponding to atom
  definitions not available on the system. Indeed, there is no comparison of
  the symbols with those in an Atom reference list in this function. See the
  validate() function for a more in-depth check of the formula.

  \return true upon successful check, false otherwise.

  \sa validate().

*/
bool
Formula::checkSyntax() const
{
  // The default formula is always m_formula.

  return checkSyntax(m_formula, m_forceCountIndex);
}


//! Validate the formula.
/*! The validation of the formula involves:

  - Checking that the formula is not empty;

  - Splitting that formula into its plus-/minus- parts and parse the obtained
  plus-/minus- formulas. During parsing of the minus-/plus- formulas, each
  atom symbol encountered in the formulas is validated against the \p refList
  reference atom list;

  - Checking that at least the plus- or the minus- part contains
  something (same idea that the formula cannot be empty).

  \param refList List of reference atoms.

  \param store Indicates if AtomCount objects created during the
  parsing of the sub-formulas generated by the split of the formula
  have to be stored, or not. Defaults to false.

  \param reset Indicates if the list of AtomCount objects has to be reset
  before the splitParts work. This parameter may be useful in case the caller
  needs to "accumulate" multiple times an accounting of the formula. Defaults
  to false.

  \return true if the validation succeeded, false otherwise.
  */
bool
Formula::validate(const QList<Atom *> &refList, bool store, bool reset)
{
  if(!refList.size())
    qFatal("Programming error.");

  if(!m_formula.size())
    return false;

  int result = splitParts(refList, 1, store, reset);

  if(result == FORMULA_SPLIT_FAIL)
    return false;

  // The sum of m_plusFormula and m_minusFormula cannot be empty.
  if(m_plusFormula.size() && !m_plusFormula.size())
    return false;

  return true;
}


//! Account \p this formula's mono/avg masses.
/*!

  The masses corresponding to the \c m_formula member are calculated first and
  then the \p mono and \p avg parameters are updated using the calculated
  values. The formula accounting to the masses can be compounded by the \p
  times factor.

  \param refList List of atoms to be used as reference.

  \param mono Pointer to the monoisotopic mass to be updated. Defaults
  to Q_NULLPTR, in which case the value is not updated.

  \param avg Pointer to the average mass to be updated. Defaults to Q_NULLPTR,
  in which case the value is not updated.

  \param times Factor by which the accounting of the \c m_formula to the masses
  should be compounded.

  \return true upon success, false otherwise, for example if the splitParts()
  call fails.

  \sa splitParts().

*/
bool
Formula::accountMasses(const QList<Atom *> &refList,
                       double *mono,
                       double *avg,
                       int times)
{
  // Note the 'times' param below.
  if(splitParts(refList, times, true /* store */, true /* reset */) ==
     FORMULA_SPLIT_FAIL)
    return false;

  // qDebug() << "accountMasses:"
  // << "after splitParts:"
  // << "store: true ; reset: true"
  // << "m_formula:" << m_formula
  // << "text" << Formula::text();

  for(int iter = 0; iter < m_atomCountList.size(); ++iter)
    {
      AtomCount *atomCount = 0;

      atomCount = m_atomCountList.at(iter);

      // note the '1' times below because we already accounted
      // for the 'times' parameter in the splitParts() call.
      if(!atomCount->accountMasses(refList, mono, avg, 1))
        return false;
    }

  return true;
}


//! Account \p this \c m_formula to the \p Ponderable.
/*!

  The masses corresponding to the \c m_formula member are calculated first and
  then \p ponderable is updated using the calculated values. The formula
  accounting to \p ponderable can be compounded by the \p times factor.

  \param refList List of atoms to be used as reference.

  \param ponderable Pointer to the ponderable to be updated. Cannot be
  Q_NULLPTR.

  \param times Factor by which the accounting of the \c m_formula to \p
  ponderable should be compounded.

  \return true upon success, false otherwise, for example if the call to
  splitPart() fails.

  \sa splitParts().

*/
bool
Formula::accountMasses(const QList<Atom *> &refList,
                       Ponderable *ponderable,
                       int times)
{
  if(ponderable == Q_NULLPTR)
    qFatal("Programming error.");

  // Note the 'times' param below.
  if(splitParts(refList, times, true, true) == FORMULA_SPLIT_FAIL)
    return false;

  for(int iter = 0; iter < m_atomCountList.size(); ++iter)
    {
      AtomCount *atomCount = m_atomCountList.at(iter);

      // Note the '1' times below because we already accounted
      // for the 'times' parameter in the splitParts() call.
      if(!atomCount->accountMasses(
           refList, &ponderable->rmono(), &ponderable->ravg(), 1))
        return false;
    }

  return true;
}


//! Account the atoms in \c this \c m_formula.
/*!

  Calls splitParts() to actually parse \c m_formula and account its atoms in
  \c m_atomCountList.

  \param refList List of atoms to be used as reference.

  \param times Factor by which the accounting of \c m_formula should be
  compounded.

  \return true upon success, false otherwise, if the call to splitParts() fails.

  \sa splitParts().
  */
bool
Formula::accountAtoms(const QList<Atom *> &refList, int times)
{
  // Note the 'times' param below.
  if(splitParts(refList, times, true, false) == FORMULA_SPLIT_FAIL)
    return false;

  return true;
}


//! Compute a formula string.
/*!

  Computes a formula string by iterating in the list of atom count objects.

  We want to provide for each positive and negative components of the \c
  m_formula, an elemental formula that complies with the convention : first
  the C atom, next the H, N, O, S, P atoms and all the subsequent ones in
  alphabetical order.

  \return A string containing the chemical formula.

*/
QString
Formula::elementalComposition(
  std::vector<std::pair<QString, int>> *dataVector) const
{
  // We have a list of AtomCount objects which might have either a
  // positive or a negative count.

  // We want to provide a formula that lists the positive component
  // first and the negative component last.

  // Each positive/negative component will list the atoms in the
  // conventional order : CxxHxxNxxOxx and all the rest in
  // alphabetical order.

  // For each atomCount in the m_atomCountList list, prepare a
  // string like "C12" and "H13", for example, in two distinct
  // QStringList's, one for positive count values and one for
  // negative count values.

  QStringList negativeStringList;
  QStringList positiveStringList;

  for(int iter = 0; iter < m_atomCountList.size(); ++iter)
    {
      AtomCount *atomCount = m_atomCountList.at(iter);

      if(atomCount->count() < 0)
        {
          negativeStringList.append(QString("%1%2")
                                      .arg(atomCount->symbol())
                                      .arg(-1 * atomCount->count()));

          // qDebug() << "negative atomCount:"
          // << atomCount->symbol()
          // << "/"
          // << (-1 * atomCount->count());
        }
      else
        {
          positiveStringList.append(
            QString("%1%2").arg(atomCount->symbol()).arg(atomCount->count()));

          // qDebug() << "positive atomCount:"
          // << atomCount->symbol()
          // << "/"
          // << atomCount->count();
        }
    }

  // Sort the lists.

  negativeStringList.sort();
  positiveStringList.sort();

  // We want to provide for each positive and negative components of the
  // initial formula object, an elemental formula that complies with the
  // convention : first the C atom, next the H, N, O, S, P atoms and all the
  // subsequent ones in alphabetical order.

  // Thus we look for the four C, H, N, O, S,P atoms, and we create the
  // initial part of the elemental formula. Each time we find one
  // such atom we remove it from the list, so that we can later just
  // append all the remaining atoms, since we have sorted the lists
  // above.

  // The positive component
  // ======================

  int atomIndex = 0;
  QString positiveComponentString;

  // Carbon
  atomIndex = positiveStringList.indexOf(QRegExp("C\\d+"));
  if(atomIndex != -1)
    {
      positiveComponentString += positiveStringList.at(atomIndex);
      positiveStringList.removeAt(atomIndex);

      if(dataVector)
        dataVector->push_back(
          std::pair<QString, int>("C", atomSymbolCount("C")));
    }

  // Hydrogen
  atomIndex = positiveStringList.indexOf(QRegExp("H\\d+"));
  if(atomIndex != -1)
    {
      positiveComponentString += positiveStringList.at(atomIndex);
      positiveStringList.removeAt(atomIndex);

      if(dataVector)
        dataVector->push_back(
          std::pair<QString, int>("H", atomSymbolCount("H")));
    }

  // Nitrogen
  atomIndex = positiveStringList.indexOf(QRegExp("N\\d+"));
  if(atomIndex != -1)
    {
      positiveComponentString += positiveStringList.at(atomIndex);
      positiveStringList.removeAt(atomIndex);

      if(dataVector)
        dataVector->push_back(
          std::pair<QString, int>("N", atomSymbolCount("N")));
    }

  // Oxygen
  atomIndex = positiveStringList.indexOf(QRegExp("O\\d+"));
  if(atomIndex != -1)
    {
      positiveComponentString += positiveStringList.at(atomIndex);
      positiveStringList.removeAt(atomIndex);

      if(dataVector)
        dataVector->push_back(
          std::pair<QString, int>("O", atomSymbolCount("O")));
    }

  // Sulfur
  atomIndex = positiveStringList.indexOf(QRegExp("S\\d+"));
  if(atomIndex != -1)
    {
      positiveComponentString += positiveStringList.at(atomIndex);
      positiveStringList.removeAt(atomIndex);

      if(dataVector)
        dataVector->push_back(
          std::pair<QString, int>("S", atomSymbolCount("S")));
    }

  // Phosphorus
  atomIndex = positiveStringList.indexOf(QRegExp("P\\d+"));
  if(atomIndex != -1)
    {
      positiveComponentString += positiveStringList.at(atomIndex);
      positiveStringList.removeAt(atomIndex);

      if(dataVector)
        dataVector->push_back(
          std::pair<QString, int>("P", atomSymbolCount("P")));
    }

  // Go on with all the other ones, if any...

  for(int iter = 0; iter < positiveStringList.size(); ++iter)
    {
      positiveComponentString += positiveStringList.at(iter);

      QRegularExpression regexp("([A-Z][a-z]*)(\\d+)");
      QRegularExpressionMatch match = regexp.match(positiveStringList.at(iter));

      if(match.hasMatch())
        {
          QString symbol  = match.captured(1);
          QString howMany = match.captured(2);

          bool ok   = false;
          int count = howMany.toInt(&ok, 10);

          if(!count && !ok)
            qFatal("Programming error.");

          if(dataVector)
            dataVector->push_back(std::pair<QString, int>(symbol, count));
        }
    }

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


  // The negative component
  // ======================

  atomIndex = 0;
  QString negativeComponentString;

  // Carbon
  atomIndex = negativeStringList.indexOf(QRegExp("C\\d+"));
  if(atomIndex != -1)
    {
      negativeComponentString += negativeStringList.at(atomIndex);
      negativeStringList.removeAt(atomIndex);

      if(dataVector)
        dataVector->push_back(
          std::pair<QString, int>("C", atomSymbolCount("C")));
    }

  // Hydrogen
  atomIndex = negativeStringList.indexOf(QRegExp("H\\d+"));
  if(atomIndex != -1)
    {
      negativeComponentString += negativeStringList.at(atomIndex);
      negativeStringList.removeAt(atomIndex);

      if(dataVector)
        dataVector->push_back(
          std::pair<QString, int>("H", atomSymbolCount("H")));
    }

  // Nitrogen
  atomIndex = negativeStringList.indexOf(QRegExp("N\\d+"));
  if(atomIndex != -1)
    {
      negativeComponentString += negativeStringList.at(atomIndex);
      negativeStringList.removeAt(atomIndex);

      if(dataVector)
        dataVector->push_back(
          std::pair<QString, int>("N", atomSymbolCount("N")));
    }

  // Oxygen
  atomIndex = negativeStringList.indexOf(QRegExp("O\\d+"));
  if(atomIndex != -1)
    {
      negativeComponentString += negativeStringList.at(atomIndex);
      negativeStringList.removeAt(atomIndex);

      if(dataVector)
        dataVector->push_back(
          std::pair<QString, int>("O", atomSymbolCount("O")));
    }

  // Sulfur
  atomIndex = negativeStringList.indexOf(QRegExp("S\\d+"));
  if(atomIndex != -1)
    {
      negativeComponentString += negativeStringList.at(atomIndex);
      negativeStringList.removeAt(atomIndex);

      if(dataVector)
        dataVector->push_back(
          std::pair<QString, int>("S", atomSymbolCount("S")));
    }

  // Phosphorus
  atomIndex = negativeStringList.indexOf(QRegExp("P\\d+"));
  if(atomIndex != -1)
    {
      negativeComponentString += negativeStringList.at(atomIndex);
      negativeStringList.removeAt(atomIndex);

      if(dataVector)
        dataVector->push_back(
          std::pair<QString, int>("P", atomSymbolCount("P")));
    }

  // Go on with all the other ones, if any...

  for(int iter = 0; iter < negativeStringList.size(); ++iter)
    {
      negativeComponentString += negativeStringList.at(iter);

      QRegularExpression regexp("([A-Z][a-z]*)(\\d+)");
      QRegularExpressionMatch match = regexp.match(negativeStringList.at(iter));

      if(match.hasMatch())
        {
          QString symbol  = match.captured(1);
          QString howMany = match.captured(2);

          bool ok   = false;
          int count = howMany.toInt(&ok, 10);

          if(!count && !ok)
            qFatal("Programming error.");

          if(dataVector)
            dataVector->push_back(std::pair<QString, int>(symbol, count));
        }
    }


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

  // Create the final elemental formula that comprises both the
  // positive and negative element. First the positive element and
  // then the negative one. Only append the negative one, prepended
  // with '-' if the string is non-empty.

  QString elementalComposition = positiveComponentString;

  if(!negativeComponentString.isEmpty())
    elementalComposition += QString("-%1").arg(negativeComponentString);

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

  return elementalComposition;
}


//! Compute the total number of atoms.
/*!

  Iterates in the \m m_atomCountList and for each AtomCount instance
  increments a total atom count by the count of that AtomCount instance.

  \return The number of atoms.

*/
int
Formula::totalAtoms() const
{
  int totalAtomCount = 0;

  for(int iter = 0; iter < m_atomCountList.size(); ++iter)
    {
      AtomCount *atomCount = m_atomCountList.at(iter);

      totalAtomCount += atomCount->count();
    }

  return totalAtomCount;
}


//! Compute the total number of isotopes.
/*!

  Iterate in the \m m_atomCountList and for each AtomCount instance finds the
  corresponding Atom in \p refList. The Atom is then used to get the number of
  Isotope instances it is made of. These are accounted for in a total count.

  \param refList List of atoms to be used as reference.

  \return The total count of isotopes in \c this \c m_atomCountList member.
  */
int
Formula::totalIsotopes(const QList<Atom *> &refList) const
{
  int totalIsotopeCount = 0;

  for(int iter = 0; iter < m_atomCountList.size(); ++iter)
    {
      AtomCount *atomCount = m_atomCountList.at(iter);

      Atom listAtom;

      if(Atom::symbolIndex(atomCount->symbol(), refList, &listAtom) == -1)
        return -1;

      // The number of isotopes for current atomCount is the number of
      // atoms compounded by the number of isotopes in the isotope
      // list.
      totalIsotopeCount += (listAtom.isotopeList().size() * atomCount->count());
    }

  return totalIsotopeCount;
}


int
Formula::atomSymbolCount(const QString &symbol) const
{
  // Search in the atom count list, how my symbol atoms there are.

  for(int iter = 0; iter < m_atomCountList.size(); ++iter)
    {
      AtomCount *atomCount = m_atomCountList.at(iter);

      if(atomCount->symbol() == symbol)
        return atomCount->count();
    }

  return 0;
}


//! Compute the number of entities (atoms, isotopes).
/*!

  Equivalent to calling separately the totalAtoms() and totalIsotopes()
  functions and setting the returned values to \p totalAtoms and \p
  totalIsotopes.

  \param refList List of atoms to be used as reference.

  \param totalAtoms Pointer to a integer in which to store the number of
  atoms. Defaults to Q_NULLPTR, in which case the value is not updated.

  \param totalIsotopes Pointer to a integer in which to store the number of
  isotopes. Defaults to Q_NULLPTR, in which case the value is not updated.

  \return true upon a successfull computation, false otherwise.
  */
bool
Formula::totalEntities(const QList<Atom *> &refList,
                       int *totalAtoms,
                       int *totalIsotopes) const
{
  for(int iter = 0; iter < m_atomCountList.size(); ++iter)
    {
      AtomCount *atomCount = m_atomCountList.at(iter);

      if(totalAtoms)
        *totalAtoms += atomCount->count();

      if(totalIsotopes)
        {
          Atom listAtom;

          if(Atom::symbolIndex(atomCount->symbol(), refList, &listAtom) == -1)
            return false;

          // The number of isotopes for current atomCount is the number of
          // atoms compounded per the number of isotopes in the isotope
          // list.
          *totalIsotopes +=
            (listAtom.isotopeList().size() * atomCount->count());
        }
    }

  return true;
}


//! Perform a deep copy of the atom count objects.
/*!

  Each AtomCount object in the member \c m_atomCountList is updated deeply by
  deep copy of the corresponding Atom as found in the Atom \p refList
  reference list passed as parameter. This ensures that each AtomCount object
  in the formula has a deep knowledge of its isotopic composition. Such kind
  of process is typically used when isotopic pattern calculations are to be
  performed for a given formula.

  \param refList List of reference atoms.

  \return true upon success, false otherwise.

*/
bool
Formula::deepAtomCopy(const QList<Atom *> &refList)
{
  // When the formula is parsed, the atomCount objects(derived form
  // Atom) are created by only shallow-copying(only the atom
  // symbol is actually copied to identify the atom).

  // Here, we are asked that the Atom component of the AtomCount
  // objects in the list of such instances be deep-copied from the
  // corresponding Atom found in the reference atom list
  // 'refList'. This way, the updated objects have their actual list
  // of isotopes(this is useful for the isotopic pattern calculation,
  // for example).

  for(int iter = 0; iter < m_atomCountList.size(); ++iter)
    {
      AtomCount *atomCount = m_atomCountList.at(iter);

      if(Atom::symbolIndex(atomCount->symbol(), refList, atomCount) == -1)
        return false;
    }

  return true;
}


//! Parse a formula XML element and set the data to \c m_formula.
/*!

  Parses the formula XML element passed as argument and sets the
  data of that element to \p this formula instance(this is called XML
  rendering). The syntax of the parsed formula is checked and the
  result of that check is returned.

  \param element XML element to be parsed and rendered.

  \return true if parsing and syntax checking were successful, false
  otherwise.

  */
bool
Formula::renderXmlFormulaElement(const QDomElement &element)
{
  if(element.tagName() != "formula")
    return false;

  m_formula = element.text();

  // Do not forget that we might have a title associated with the
  // formula and spaces. checkSyntax() should care of removing these
  // title and spaces before checking for chemical syntax
  // correctness.

  return checkSyntax();
}

} // namespace minexpert

} // namespace msxps
