//
//  Copyright (C) 2013-2021 Paolo Tosco
//
//  Copyright (C) 2004-2006 Rational Discovery LLC
//
//   @@ All Rights Reserved @@
//  This file is part of the RDKit.
//  The contents are covered by the terms of the BSD license
//  which is included in the file license.txt, found at the root
//  of the RDKit source tree.
//
#include <GraphMol/RDKitBase.h>
#include <GraphMol/MolOps.h>
#include <ForceField/MMFF/Nonbonded.h>
#include <RDGeneral/Invariant.h>
#include <RDGeneral/RDLog.h>
#include <boost/dynamic_bitset.hpp>

#include <GraphMol/QueryOps.h>
#include "AtomTyper.h"
#include <cstdarg>

namespace RDKit {
namespace MMFF {
using namespace ForceFields::MMFF;
namespace DefaultParameters {

const MMFFPropCollection *getMMFFProp() {
  static MMFFPropCollection ds_instance("");
  return &ds_instance;
}

const MMFFAromCollection *getMMFFArom() {
  static MMFFAromCollection ds_instance;
  return &ds_instance;
}

const MMFFBndkCollection *getMMFFBndk() {
  static MMFFBndkCollection ds_instance;
  return &ds_instance;
}

const MMFFBondCollection *getMMFFBond() {
  static MMFFBondCollection ds_instance;
  return &ds_instance;
}

const MMFFChgCollection *getMMFFChg() {
  static MMFFChgCollection ds_instance;
  return &ds_instance;
}

const MMFFDefCollection *getMMFFDef() {
  static MMFFDefCollection ds_instance;
  return &ds_instance;
}

const MMFFHerschbachLaurieCollection *getMMFFHerschbachLaurie() {
  static MMFFHerschbachLaurieCollection ds_instance;
  return &ds_instance;
}

const MMFFPBCICollection *getMMFFPBCI() {
  static MMFFPBCICollection ds_instance;
  return &ds_instance;
}

const MMFFAngleCollection *getMMFFAngle() {
  static MMFFAngleCollection ds_instance;
  return &ds_instance;
}

const MMFFStbnCollection *getMMFFStbn() {
  static MMFFStbnCollection ds_instance;
  return &ds_instance;
}

const MMFFDfsbCollection *getMMFFDfsb() {
  static MMFFDfsbCollection ds_instance;
  return &ds_instance;
}

const MMFFCovRadPauEleCollection *getMMFFCovRadPauEle() {
  static MMFFCovRadPauEleCollection ds_instance;
  return &ds_instance;
}

const MMFFTorCollection *getMMFFTor(const bool isMMFFs) {
  static MMFFTorCollection MMFF94(false, "");
  static MMFFTorCollection MMFF94s(true, "");
  return (isMMFFs) ? &MMFF94s : &MMFF94;
}

const MMFFOopCollection *getMMFFOop(const bool isMMFFs) {
  static MMFFOopCollection MMFF94(false, "");
  static MMFFOopCollection MMFF94s(true, "");
  return (isMMFFs) ? &MMFF94s : &MMFF94;
}

const MMFFVdWCollection *getMMFFVdW() {
  static MMFFVdWCollection ds_instance;
  return &ds_instance;
}

}  // namespace DefaultParameters
class RingMembership {
 public:
  RingMembership() {};
  bool getIsInAromaticRing() const { return d_isInAromaticRing; }
  void setIsInAromaticRing(bool isInAromaticRing) {
    d_isInAromaticRing = isInAromaticRing;
  }
  const std::set<std::uint32_t> &getRingIdxSet() const { return d_ringIdxSet; }
  std::set<std::uint32_t> &getRingIdxSet() { return d_ringIdxSet; }

 private:
  bool d_isInAromaticRing{false};
  std::set<std::uint32_t> d_ringIdxSet;
};

class RingMembershipSize {
  typedef std::map<unsigned int, RingMembership> RingMembershipMap;
  typedef std::map<unsigned int, RingMembershipMap> RingSizeMembershipMap;

 public:
  static const std::uint32_t IS_AROMATIC_BIT;
  RingMembershipSize(const ROMol &mol);
  bool isAtomInAromaticRingOfSize(const Atom *atom,
                                  const unsigned int ringSize) const;
  bool areAtomsInSameAromaticRing(const Atom *atom1, const Atom *atom2) const;
  bool areAtomsInSameRingOfSize(const unsigned int ringSize,
                                const unsigned int numAtoms, ...) const;

 private:
  RingSizeMembershipMap d_ringSizeMembershipMap;
};

const std::uint32_t RingMembershipSize::IS_AROMATIC_BIT = (1 << 31);

RingMembershipSize::RingMembershipSize(const ROMol &mol) {
  static const unsigned int MAX_NUM_RINGS = (0xFFFFFFFF >> 1);
  const RingInfo *ringInfo = mol.getRingInfo();
  const VECT_INT_VECT &atomRings = ringInfo->atomRings();
  PRECONDITION(atomRings.size() < MAX_NUM_RINGS, "Too many rings");
  for (std::uint32_t ringIdx = 0; ringIdx < atomRings.size(); ++ringIdx) {
    unsigned int ringSize = atomRings[ringIdx].size();
    std::uint32_t ringIdxWithAromaticFlag = ringIdx;
    bool ringIsAromatic = isRingAromatic(mol, atomRings[ringIdx]);
    if (ringIsAromatic) {
      ringIdxWithAromaticFlag |= IS_AROMATIC_BIT;
    }
    auto it = d_ringSizeMembershipMap.find(ringSize);
    if (it == d_ringSizeMembershipMap.end()) {
      it = d_ringSizeMembershipMap
               .insert(std::make_pair(ringSize, RingMembershipMap()))
               .first;
    }
    for (int atomIdxIt : atomRings[ringIdx]) {
      auto it2 = it->second.find(atomIdxIt);
      if (it2 == it->second.end()) {
        it2 = it->second.insert(std::make_pair(atomIdxIt, RingMembership()))
                  .first;
      }
      it2->second.getRingIdxSet().insert(ringIdxWithAromaticFlag);
      if (ringIsAromatic) {
        it2->second.setIsInAromaticRing(true);
      }
    }
  }
}

// returns true if the atom is in an aromatic ring of size ringSize
bool RingMembershipSize::isAtomInAromaticRingOfSize(
    const Atom *atom, const unsigned int ringSize) const {
  auto it = d_ringSizeMembershipMap.find(ringSize);
  bool isAromatic = (it != d_ringSizeMembershipMap.end());
  if (isAromatic) {
    auto it2 = it->second.find(atom->getIdx());
    isAromatic = (it2 != it->second.end());
    if (isAromatic) {
      isAromatic = it2->second.getIsInAromaticRing();
    }
  }

  return isAromatic;
}

bool RingMembershipSize::areAtomsInSameAromaticRing(const Atom *atom1,
                                                    const Atom *atom2) const {
  bool areInSameAromaticRing = false;

  for (auto it = d_ringSizeMembershipMap.begin();
       !areInSameAromaticRing && (it != d_ringSizeMembershipMap.end()); ++it) {
    std::vector<std::uint32_t> intersectVect;
    auto it1 = it->second.find(atom1->getIdx());
    auto it2 = it->second.find(atom2->getIdx());
    if ((it1 != it->second.end()) && (it2 != it->second.end())) {
      std::set_intersection(it1->second.getRingIdxSet().begin(),
                            it1->second.getRingIdxSet().end(),
                            it2->second.getRingIdxSet().begin(),
                            it2->second.getRingIdxSet().end(),
                            std::back_inserter(intersectVect));
      for (std::vector<std::uint32_t>::const_iterator ivIt =
               intersectVect.begin();
           !areInSameAromaticRing && (ivIt != intersectVect.end()); ++ivIt) {
        areInSameAromaticRing = *ivIt & IS_AROMATIC_BIT;
      }
    }
  }

  return areInSameAromaticRing;
}

bool RingMembershipSize::areAtomsInSameRingOfSize(const unsigned int ringSize,
                                                  const unsigned int numAtoms,
                                                  ...) const {
  va_list atoms;
  bool areInSameRingOfSize = false;

  auto it = d_ringSizeMembershipMap.find(ringSize);
  if (it != d_ringSizeMembershipMap.end()) {
    va_start(atoms, numAtoms);
    unsigned int idx1 = va_arg(atoms, const Atom *)->getIdx();
    auto it1 = it->second.find(idx1);
    if (it1 != it->second.end()) {
      std::set<std::uint32_t> commonSet = it1->second.getRingIdxSet();
      for (unsigned int i = 1; !commonSet.empty() && (i < numAtoms); ++i) {
        areInSameRingOfSize = false;
        unsigned int idx2 = va_arg(atoms, const Atom *)->getIdx();
        auto it2 = it->second.find(idx2);
        if (it2 == it->second.end()) {
          break;
        }
        std::set<std::uint32_t> intersect;
        std::set_intersection(commonSet.begin(), commonSet.end(),
                              it2->second.getRingIdxSet().begin(),
                              it2->second.getRingIdxSet().end(),
                              std::inserter(intersect, intersect.end()));
        commonSet = intersect;
        areInSameRingOfSize = !commonSet.empty();
      }
    }
    va_end(atoms);
  }

  return areInSameRingOfSize;
}

// given the atomic num, this function returns the periodic
// table row number, starting from 0 for hydrogen
unsigned int getPeriodicTableRow(const int atomicNum) {
  unsigned int periodicTableRow = 0;

  if ((atomicNum >= 3) && (atomicNum <= 10)) {
    periodicTableRow = 1;
  } else if ((atomicNum >= 11) && (atomicNum <= 18)) {
    periodicTableRow = 2;
  } else if ((atomicNum >= 19) && (atomicNum <= 36)) {
    periodicTableRow = 3;
  } else if ((atomicNum >= 37) && (atomicNum <= 54)) {
    periodicTableRow = 4;
  }

  return periodicTableRow;
}

// given the atomic num, this function returns the periodic
// table row number, starting from 1 for helium
// Hydrogen has a special row number (0), while transition
// metals have the row number multiplied by 10
unsigned int getPeriodicTableRowHL(const int atomicNum) {
  unsigned int periodicTableRow = 0;

  if (atomicNum == 2) {
    periodicTableRow = 1;
  } else if ((atomicNum >= 3) && (atomicNum <= 10)) {
    periodicTableRow = 2;
  } else if ((atomicNum >= 11) && (atomicNum <= 18)) {
    periodicTableRow = 3;
  } else if ((atomicNum >= 19) && (atomicNum <= 36)) {
    periodicTableRow = 4;
  } else if ((atomicNum >= 37) && (atomicNum <= 54)) {
    periodicTableRow = 5;
  }
  if (((atomicNum >= 21) && (atomicNum <= 30)) ||
      ((atomicNum >= 39) && (atomicNum <= 48))) {
    periodicTableRow *= 10;
  }

  return periodicTableRow;
}

// given the MMFF atom type, this function returns true
// if it is aromatic
bool isAromaticAtomType(const unsigned int atomType) {
  static const unsigned int aromatic_array[] = {
      37, 38, 39, 44, 58, 59, 63, 64, 65, 66, 69, 76, 78, 79, 80, 81, 82};
  const std::set<unsigned int> aromaticTypes(
      aromatic_array,
      aromatic_array + sizeof(aromatic_array) / sizeof(aromatic_array[0]));

  return (aromaticTypes.find(atomType) != aromaticTypes.end());
}

bool isRingAromatic(const ROMol &mol, const INT_VECT &ringIndxVect) {
  bool isAromatic = true;
  for (unsigned int i = 0; isAromatic && (i < ringIndxVect.size() - 1); ++i) {
    isAromatic = (mol.getBondBetweenAtoms(ringIndxVect[i], ringIndxVect[i + 1])
                      ->getBondType() == Bond::AROMATIC);
  }
  return isAromatic;
}

// returns true if the atom is in a ring of size ringSize
bool isAtomInAromaticRingOfSize(const Atom *atom, const unsigned int ringSize) {
  bool isAromatic = false;
  const ROMol &mol = atom->getOwningMol();
  const VECT_INT_VECT &atomRings = mol.getRingInfo()->atomRings();

  if (atom->getIsAromatic()) {
    for (unsigned int i = 0; (!isAromatic) && (i < atomRings.size()); ++i) {
      if ((atomRings[i].size() != ringSize) ||
          (std::find(atomRings[i].begin(), atomRings[i].end(),
                     atom->getIdx()) == atomRings[i].end())) {
        continue;
      }
      isAromatic = isRingAromatic(mol, atomRings[i]);
    }
  }

  return isAromatic;
}

// returns true if the atom is an N-oxide
bool isAtomNOxide(const Atom *atom) {
  bool isNOxide = false;
  const ROMol &mol = atom->getOwningMol();
  ROMol::ADJ_ITER nbrIdx;
  ROMol::ADJ_ITER endNbrs;

  if ((atom->getAtomicNum() == 7) && (atom->getTotalDegree() >= 3)) {
    // loop over neighbors
    boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
    for (; (!isNOxide) && (nbrIdx != endNbrs); ++nbrIdx) {
      const Atom *nbrAtom = mol[*nbrIdx];
      isNOxide =
          ((nbrAtom->getAtomicNum() == 8) && (nbrAtom->getTotalDegree() == 1));
    }
  }

  return isNOxide;
}

// if the angle formed by atoms with indexes idx1, idx2, idx3
// is in a ring of {3,4} atoms returns 3 or 4, respectively;
// otherwise it returns 0
unsigned int isAngleInRingOfSize3or4(const ROMol &mol, const unsigned int idx1,
                                     const unsigned int idx2,
                                     const unsigned int idx3) {
  unsigned int ringSize = 0;

  if (mol.getBondBetweenAtoms(idx1, idx2) &&
      mol.getBondBetweenAtoms(idx2, idx3)) {
    if (mol.getBondBetweenAtoms(idx3, idx1)) {
      ringSize = 3;
    } else {
      std::set<unsigned int> s1;
      std::set<unsigned int> s2;
      std::vector<int> intersect;
      ROMol::ADJ_ITER nbrIdx;
      ROMol::ADJ_ITER endNbrs;
      unsigned int newIdx;
      boost::tie(nbrIdx, endNbrs) =
          mol.getAtomNeighbors(mol.getAtomWithIdx(idx1));
      for (; nbrIdx != endNbrs; ++nbrIdx) {
        newIdx = mol[*nbrIdx]->getIdx();
        if (newIdx != idx2) {
          s1.insert(newIdx);
        }
      }
      boost::tie(nbrIdx, endNbrs) =
          mol.getAtomNeighbors(mol.getAtomWithIdx(idx3));
      for (; nbrIdx != endNbrs; ++nbrIdx) {
        newIdx = mol[*nbrIdx]->getIdx();
        if (newIdx != idx2) {
          s2.insert(newIdx);
        }
      }
      std::set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(),
                            std::back_inserter(intersect));
      if (intersect.size()) {
        ringSize = 4;
      }
    }
  }

  return ringSize;
}

// if the dihedral angle formed by atoms with indexes idx1,
// idx2, idx3, idx4 is in a ring of {4,5} atoms returns 4 or 5,
// respectively; otherwise it returns 0
unsigned int isTorsionInRingOfSize4or5(const ROMol &mol,
                                       const unsigned int idx1,
                                       const unsigned int idx2,
                                       const unsigned int idx3,
                                       const unsigned int idx4) {
  unsigned int ringSize = 0;

  if (mol.getBondBetweenAtoms(idx1, idx2) &&
      mol.getBondBetweenAtoms(idx2, idx3) &&
      mol.getBondBetweenAtoms(idx3, idx4)) {
    if (mol.getBondBetweenAtoms(idx4, idx1)) {
      ringSize = 4;
    } else {
      std::set<unsigned int> s1;
      std::set<unsigned int> s2;
      std::vector<int> intersect;
      ROMol::ADJ_ITER nbrIdx;
      ROMol::ADJ_ITER endNbrs;
      unsigned int newIdx;
      boost::tie(nbrIdx, endNbrs) =
          mol.getAtomNeighbors(mol.getAtomWithIdx(idx1));
      for (; nbrIdx != endNbrs; ++nbrIdx) {
        newIdx = mol[*nbrIdx]->getIdx();
        if (newIdx != idx2) {
          s1.insert(newIdx);
        }
      }
      boost::tie(nbrIdx, endNbrs) =
          mol.getAtomNeighbors(mol.getAtomWithIdx(idx4));
      for (; nbrIdx != endNbrs; ++nbrIdx) {
        newIdx = mol[*nbrIdx]->getIdx();
        if (newIdx != idx3) {
          s2.insert(newIdx);
        }
      }
      std::set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(),
                            std::back_inserter(intersect));
      if (intersect.size()) {
        ringSize = 5;
      }
    }
  }

  return ringSize;
}

// return true if atoms are in the same ring of size ringSize
bool areAtomsInSameRingOfSize(const ROMol &mol, const unsigned int ringSize,
                              const unsigned int numAtoms, ...) {
  unsigned int i;
  bool areInSameRingOfSize = false;
  const VECT_INT_VECT &atomRings = mol.getRingInfo()->atomRings();
  unsigned int idx;
  va_list atomIdxs;

  for (i = 0; (!areInSameRingOfSize) && (i < atomRings.size()); ++i) {
    if (atomRings[i].size() != ringSize) {
      continue;
    }
    areInSameRingOfSize = true;
    va_start(atomIdxs, numAtoms);
    for (unsigned int j = 0; areInSameRingOfSize && (j < numAtoms); ++j) {
      idx = va_arg(atomIdxs, unsigned int);
      areInSameRingOfSize = (std::find(atomRings[i].begin(), atomRings[i].end(),
                                       idx) != atomRings[i].end());
    }
    va_end(atomIdxs);
  }

  return areInSameRingOfSize;
}

// return true if atoms are in the same aromatic ring
bool areAtomsInSameAromaticRing(const ROMol &mol, const unsigned int idx1,
                                const unsigned int idx2) {
  unsigned int i;
  unsigned int j;
  bool areInSameAromatic = false;
  const VECT_INT_VECT &atomRings = mol.getRingInfo()->atomRings();

  if (mol.getAtomWithIdx(idx1)->getIsAromatic() &&
      mol.getAtomWithIdx(idx2)->getIsAromatic()) {
    for (i = 0; (!areInSameAromatic) && (i < atomRings.size()); ++i) {
      if ((std::find(atomRings[i].begin(), atomRings[i].end(), idx1) !=
           atomRings[i].end()) &&
          (std::find(atomRings[i].begin(), atomRings[i].end(), idx2) !=
           atomRings[i].end())) {
        areInSameAromatic = true;
        for (j = 0; areInSameAromatic && (j < (atomRings[i].size() - 1)); ++j) {
          areInSameAromatic =
              (mol.getBondBetweenAtoms(atomRings[i][j], atomRings[i][j + 1])
                   ->getBondType() == Bond::AROMATIC);
        }
      }
    }
  }

  return areInSameAromatic;
}

// sets the MMFF atomType for a heavy atom
void MMFFMolProperties::setMMFFHeavyAtomType(const RingMembershipSize &rmSize,
                                             const Atom *atom) {
  unsigned int atomType = 0;
  unsigned int i;
  unsigned int j;
  unsigned int nTermObondedToN = 0;
  bool alphaOrBetaInSameRing = false;
  bool isAlphaOS = false;
  bool isBetaOS = false;
  bool isNSO2orNSO3orNCN = false;
  const ROMol &mol = atom->getOwningMol();
  RingInfo *ringInfo = mol.getRingInfo();
  ROMol::ADJ_ITER nbrIdx;
  ROMol::ADJ_ITER endNbrs;
  ROMol::ADJ_ITER nbr2Idx;
  ROMol::ADJ_ITER end2Nbrs;
  ROMol::ADJ_ITER nbr3Idx;
  ROMol::ADJ_ITER end3Nbrs;
  std::vector<const Atom *> alphaHet;
  std::vector<const Atom *> betaHet;

  if (atom->getIsAromatic()) {
    if (rmSize.isAtomInAromaticRingOfSize(atom, 5)) {
      // 5-membered aromatic rings
      // if ipso is carbon or nitrogen, find eventual alpha and beta heteroatoms
      if ((atom->getAtomicNum() == 6) || (atom->getAtomicNum() == 7)) {
        // loop over alpha neighbors
        boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
        for (; nbrIdx != endNbrs; ++nbrIdx) {
          const Atom *nbrAtom = mol[*nbrIdx];
          // if the alpha neighbor is not in a 5-membered aromatic
          // ring, skip to the next neighbor
          if (!rmSize.isAtomInAromaticRingOfSize(nbrAtom, 5)) {
            continue;
          }
          // if the alpha neighbor belongs to the same ring of ipso atom
          // and it is either oxygen, sulfur, or non-N-oxide trivalent nitrogen,
          // add it to the alpha atom vector
          if (rmSize.areAtomsInSameRingOfSize(5, 2, atom, nbrAtom) &&
              ((nbrAtom->getAtomicNum() == 8) ||
               (nbrAtom->getAtomicNum() == 16) ||
               ((nbrAtom->getAtomicNum() == 7) &&
                (nbrAtom->getTotalDegree() == 3) &&
                (!isAtomNOxide(nbrAtom))))) {
            alphaHet.push_back(nbrAtom);
          }
          // loop over beta neighbors
          boost::tie(nbr2Idx, end2Nbrs) = mol.getAtomNeighbors(nbrAtom);
          for (; nbr2Idx != end2Nbrs; ++nbr2Idx) {
            const Atom *nbr2Atom = mol[*nbr2Idx];
            // if we have gone back to the ipso atom, move on
            if (nbr2Atom->getIdx() == atom->getIdx()) {
              continue;
            }
            // if the beta neighbor is not in a 5-membered aromatic
            // ring, skip to the next neighbor
            if (!rmSize.isAtomInAromaticRingOfSize(nbr2Atom, 5)) {
              continue;
            }
            // if the beta neighbor belongs to the same ring of ipso atom
            // and it is either oxygen, sulfur, or non-N-oxide trivalent
            // nitrogen,
            // add it to the beta atom vector
            if (rmSize.areAtomsInSameRingOfSize(5, 2, atom, nbr2Atom) &&
                ((nbr2Atom->getAtomicNum() == 8) ||
                 (nbr2Atom->getAtomicNum() == 16) ||
                 ((nbr2Atom->getAtomicNum() == 7) &&
                  (nbr2Atom->getTotalDegree() == 3) &&
                  (!isAtomNOxide(nbr2Atom))))) {
              betaHet.push_back(nbr2Atom);
            }
          }
        }
        isAlphaOS = false;
        for (i = 0; (!isAlphaOS) && (i < alphaHet.size()); ++i) {
          isAlphaOS = ((alphaHet[i]->getAtomicNum() == 8) ||
                       (alphaHet[i]->getAtomicNum() == 16));
        }
        isBetaOS = false;
        for (i = 0; (!isBetaOS) && (i < betaHet.size()); ++i) {
          isBetaOS = ((betaHet[i]->getAtomicNum() == 8) ||
                      (betaHet[i]->getAtomicNum() == 16));
        }
        if (alphaHet.size() && betaHet.size()) {
          // do alpha and beta heteroatoms belong to the same ring?
          for (i = 0; (!alphaOrBetaInSameRing) && (i < alphaHet.size()); ++i) {
            for (j = 0; (!alphaOrBetaInSameRing) && (j < betaHet.size()); ++j) {
              alphaOrBetaInSameRing = rmSize.areAtomsInSameRingOfSize(
                  5, 2, alphaHet[i], betaHet[j]);
            }
          }
        }
      }

      switch (atom->getAtomicNum()) {
        // Carbon
        case 6:
          // if there are no beta heteroatoms
          if (!(betaHet.size())) {
            // count how many 3-neighbor nitrogens we have
            // to be CIM+, there must be at least two such nitrogens,
            // one of which in a 5-membered aromatic ring and none
            // in a 6-membered aromatic ring; additionally, one
            // one of the hydrogens must be protonated
            unsigned int nN = 0;
            unsigned int nFormalCharge = 0;
            unsigned int nInAromatic5Ring = 0;
            unsigned int nInAromatic6Ring = 0;
            boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
            for (; nbrIdx != endNbrs; ++nbrIdx) {
              const Atom *nbrAtom = mol[*nbrIdx];
              if ((nbrAtom->getAtomicNum() == 7) &&
                  (nbrAtom->getTotalDegree() == 3)) {
                ++nN;
                if ((nbrAtom->getFormalCharge() > 0) &&
                    (!isAtomNOxide(nbrAtom))) {
                  ++nFormalCharge;
                }
                if (rmSize.isAtomInAromaticRingOfSize(nbrAtom, 5)) {
                  ++nInAromatic5Ring;
                }
                if (rmSize.isAtomInAromaticRingOfSize(nbrAtom, 6)) {
                  ++nInAromatic6Ring;
                }
              }
            }
            if ((((nN == 2) && nInAromatic5Ring) ||
                 ((nN == 3) && (nInAromatic5Ring == 2))) &&
                nFormalCharge && (!nInAromatic6Ring)) {
              // CIM+
              // Aromatic carbon between N's in imidazolium
              atomType = 80;
              break;
            }
          }
          // if there are neither alpha nor beta heteroatoms
          // or if there are both, but they belong to different rings
          if (alphaHet.size() == betaHet.size()) {
            bool surroundedByBenzeneC = true;
            bool surroundedByArom = true;
            // loop over neighbors
            // are all neighbors aromatic?
            // are all neighbors benzene carbons?
            boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
            for (; nbrIdx != endNbrs; ++nbrIdx) {
              const Atom *nbrAtom = mol[*nbrIdx];

              if ((nbrAtom->getAtomicNum() != 6) ||
                  (!(ringInfo->isAtomInRingOfSize(nbrAtom->getIdx(), 6)))) {
                surroundedByBenzeneC = false;
              }
              if (rmSize.areAtomsInSameRingOfSize(5, 2, atom, nbrAtom) &&
                  (!(nbrAtom->getIsAromatic()))) {
                surroundedByArom = false;
              }
            }
            // if there are no alpha and beta heteroatoms and
            // all neighbors are aromatic but not all of them
            // benzene carbons, or if there are alpha and beta
            // atoms but they belong to different rings, or if
            // there are alpha and beta heteroatoms but no alpha
            // oxygen or sulfur, then it's C5
            if (((!(alphaHet.size())) && (!(betaHet.size())) &&
                 (!surroundedByBenzeneC) && surroundedByArom) ||
                (alphaHet.size() && betaHet.size() &&
                 ((!alphaOrBetaInSameRing) || ((!isAlphaOS) && (!isBetaOS))))) {
              // C5
              // General carbon in 5-membered heteroaromatic ring
              atomType = 78;
              break;
            }
          }
          if (alphaHet.size() && ((!(betaHet.size())) || isAlphaOS)) {
            // C5A
            // Aromatic 5-ring C, alpha to N:, O: or S:
            atomType = 63;
            break;
          }
          if (betaHet.size() && ((!(alphaHet.size())) || isBetaOS)) {
            // C5B
            // Aromatic 5-ring C, alpha to N:, O: or S:
            atomType = 64;
            break;
          }
          break;

        // Nitrogen
        case 7:
          if (isAtomNOxide(atom)) {
            // N5AX
            // N-oxide nitrogen in 5-ring alpha position
            // N5BX
            // N-oxide nitrogen in 5-ring beta position
            // N5OX
            // N-oxide nitrogen in other 5-ring position
            atomType = 82;
            break;
          }
          // if there are neither alpha nor beta heteroatoms
          if ((!(alphaHet.size())) && (!(betaHet.size()))) {
            // if it is nitrogen
            // if valence is 3, it's pyrrole nitrogen
            if (atom->getTotalDegree() == 3) {
              // NPYL
              // Aromatic 5-ring nitrogen with pi lone pair
              atomType = 39;
              break;
            }
            // otherwise it is anionic
            // N5M
            // Nitrogen in 5-ring aromatic anion
            atomType = 76;
            break;
          }
          if ((atom->getTotalDegree() == 3) &&
              alphaHet.size() != betaHet.size()) {
            // NIM+
            // Aromatic nitrogen in imidazolium
            // N5A+
            // Positive nitrogen in 5-ring alpha position
            // N5B+
            // Positive nitrogen in 5-ring beta position
            // N5+
            // Positive nitrogen in other 5-ring position
            atomType = 81;
            break;
          }
          // if there are alpha heteroatoms and either no beta heteroatoms
          // or no alpha oxygen/sulfur
          if (alphaHet.size() && ((!(betaHet.size())) || isAlphaOS)) {
            // N5A
            // Aromatic 5-ring N, alpha to N:, O: or S:
            atomType = 65;
            break;
          }
          // if there are beta heteroatoms and either no alpha heteroatoms
          // or no beta oxygen/sulfur
          if (betaHet.size() && ((!(alphaHet.size())) || isBetaOS)) {
            // N5B
            // Aromatic 5-ring N, beta to N:, O: or S:
            atomType = 66;
            break;
          }
          // if there are both alpha and beta heteroatoms
          if (alphaHet.size() && betaHet.size()) {
            // N5
            // General nitrogen in 5-memebered heteroaromatic ring
            atomType = 79;
            break;
          }
          break;

        // Oxygen
        case 8:
          // OFUR
          // Aromatic 5-ring oxygen with pi lone pair
          atomType = 59;
          break;

        // Sulfur
        case 16:
          // STHI
          // Aromatic 5-ring sulfur with pi lone pair
          atomType = 44;
          break;
      }
    }

    if (!atomType && (rmSize.isAtomInAromaticRingOfSize(atom, 6))) {
      // 6-membered aromatic rings
      switch (atom->getAtomicNum()) {
        // Carbon
        case 6:
          // CB
          // Aromatic carbon, e.g., in benzene
          atomType = 37;
          break;

        // Nitrogen
        case 7:
          if (isAtomNOxide(atom)) {
            // NPOX
            // Pyridinium N-oxide nitrogen
            atomType = 69;
            break;
          }
          if (atom->getTotalDegree() == 3) {
            // NPD+
            // Aromatic nitrogen in pyridinium
            atomType = 58;
            break;
          }
          // NPYD
          // Aromatic nitrogen with sigma lone pair
          atomType = 38;
          break;
      }
    }
  }

  if (!atomType) {
    // Aliphatic heavy atom types

    switch (atom->getAtomicNum()) {
      // Lithium
      case 3:
        if (atom->getDegree() == 0) {
          // LI+
          // Lithium cation
          atomType = 92;
          break;
        }
        break;

      // Carbon
      case 6:
        // 4 neighbors
        if (atom->getTotalDegree() == 4) {
          if (ringInfo->isAtomInRingOfSize(atom->getIdx(), 3)) {
            // CR3R
            // Aliphatic carbon in 3-membered ring
            atomType = 22;
            break;
          }
          if (ringInfo->isAtomInRingOfSize(atom->getIdx(), 4)) {
            // CR4R
            // Aliphatic carbon in 4-membered ring
            atomType = 20;
            break;
          }
          // CR
          // Alkyl carbon
          atomType = 1;
          break;
        }
        // 3 neighbors
        if (atom->getTotalDegree() == 3) {
          unsigned int nN2 = 0;
          unsigned int nN3 = 0;
          unsigned int nO = 0;
          unsigned int nS = 0;
          unsigned int doubleBondedElement = 0;
          boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
          for (; nbrIdx != endNbrs; ++nbrIdx) {
            const Atom *nbrAtom = mol[*nbrIdx];
            // find if there is a double-bonded element
            if ((mol.getBondBetweenAtoms(nbrAtom->getIdx(), atom->getIdx()))
                    ->getBondType() == Bond::DOUBLE) {
              doubleBondedElement = nbrAtom->getAtomicNum();
            }
            // count how many terminal oxygen/sulfur atoms
            // are bonded to ipso
            if (nbrAtom->getTotalDegree() == 1) {
              if (nbrAtom->getAtomicNum() == 8) {
                ++nO;
              } else if (nbrAtom->getAtomicNum() == 16) {
                ++nS;
              }
            } else if (nbrAtom->getAtomicNum() == 7) {
              // count how many nitrogens with 3 neighbors
              // are bonded to ipso
              if (nbrAtom->getTotalDegree() == 3) {
                ++nN3;
              }
              // count how many nitrogens with 2 neighbors
              // are double-bonded to ipso
              else if ((nbrAtom->getTotalDegree() == 2) &&
                       ((mol.getBondBetweenAtoms(nbrAtom->getIdx(),
                                                 atom->getIdx()))
                            ->getBondType() == Bond::DOUBLE)) {
                ++nN2;
              }
            }
          }
          // if there are two or more nitrogens with 3 neighbors each,
          // and there are no nitrogens with two neighbors only,
          // and carbon is double-bonded to nitrogen
          if ((nN3 >= 2) && (!nN2) && (doubleBondedElement == 7)) {
            // CNN+
            // Carbon in +N=C-N: resonance structures
            // CGD+
            // Guanidinium carbon
            atomType = 57;
            break;
          }
          // if there are two terminal oxygen/sulfur atoms
          if ((nO == 2) || (nS == 2)) {
            // CO2M
            // Carbon in carboxylate anion
            // CS2M
            // Carbon in thiocarboxylate anion
            atomType = 41;
            break;
          }
          // if this carbon is in a 4-membered ring and
          // is double-bonded to another carbon
          if (ringInfo->isAtomInRingOfSize(atom->getIdx(), 4) &&
              (doubleBondedElement == 6)) {
            // CR4E
            // Olefinic carbon in 4-membered ring
            atomType = 30;
            break;
          }
          // if this carbon is is double-bonded to nitrogen,
          // oxygen, phosphorus or sulfur
          if ((doubleBondedElement == 7) || (doubleBondedElement == 8) ||
              (doubleBondedElement == 15) || (doubleBondedElement == 16)) {
            // C=N
            // Imine-atomType carbon
            // CGD
            // Guanidine carbon
            // C=O
            // Generic carbonyl carbon
            // C=OR
            // Ketone or aldehyde carbonyl carbon
            // C=ON
            // Amide carbonyl carbon
            // COO
            // Carboxylic acid or ester carbonyl carbon
            // COON
            // Carbamate carbonyl carbon
            // COOO
            // Carbonic acid or ester carbonyl function
            // C=OS
            // Thioester carbonyl carbon, double bonded to O
            // C=P
            // Carbon doubly bonded to P
            // C=S
            // Thioester carbon, double bonded to S
            // C=SN
            // Thioamide carbon, double bonded to S
            // CSO2
            // Carbon in >C=SO2
            // CS=O
            // Sulfinyl carbon in >C=S=O
            // CSS
            // Thiocarboxylic acid or ester carbon
            atomType = 3;
            break;
          }
          // otherwise it must be generic sp2 carbon
          // C=C
          // Vinylic carbon
          // CSP2
          // Generic sp2 carbon
          atomType = 2;
          break;
        }
        // 2 neighbors
        if (atom->getTotalDegree() == 2) {
          // CSP
          // Acetylenic carbon
          // =C=
          // Allenic carbon
          atomType = 4;
          break;
        }
        // 1 neighbor
        if (atom->getTotalDegree() == 1) {
          // C%-
          // Isonitrile carbon
          atomType = 60;
          break;
        }
        break;

      // Nitrogen
      case 7:
        // if the neighbor is phosphorus or sulfur
        // count the number of terminal oxygens bonded
        // to that phosphorus or sulfur atom
        boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
        for (; nbrIdx != endNbrs; ++nbrIdx) {
          const Atom *nbrAtom = mol[*nbrIdx];
          // count how many terminal oxygen atoms
          // are bonded to ipso
          if ((nbrAtom->getAtomicNum() == 8) &&
              (nbrAtom->getTotalDegree() == 1)) {
            ++nTermObondedToN;
          }
          if (((atom->getValence(Atom::ValenceType::EXPLICIT) +
                atom->getNumImplicitHs()) >= 3) &&
              ((nbrAtom->getAtomicNum() == 15) ||
               (nbrAtom->getAtomicNum() == 16))) {
            unsigned int nObondedToSP = 0;
            boost::tie(nbr2Idx, end2Nbrs) = mol.getAtomNeighbors(nbrAtom);
            for (; nbr2Idx != end2Nbrs; ++nbr2Idx) {
              const Atom *nbr2Atom = mol[*nbr2Idx];
              if ((nbr2Atom->getAtomicNum() == 8) &&
                  (nbr2Atom->getTotalDegree() == 1)) {
                ++nObondedToSP;
              }
            }
            // if there are two or more oxygens, ipso is a sulfonamide nitrogen
            if (!isNSO2orNSO3orNCN) {
              isNSO2orNSO3orNCN = (nObondedToSP >= 2);
            }
          }
        }
        // 4 neighbors
        if (atom->getTotalDegree() == 4) {
          if (isAtomNOxide(atom)) {
            // N3OX
            // sp3-hybridized N-oxide nitrogen
            atomType = 68;
            break;
          }
          // NR+
          // Quaternary nitrogen
          atomType = 34;
          break;
        }
        // 3 neighbors
        if (atom->getTotalDegree() == 3) {
          // total bond order >= 4
          if ((atom->getValence(Atom::ValenceType::EXPLICIT) +
               atom->getNumImplicitHs()) >= 4) {
            bool doubleBondedCN = false;
            boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
            for (; nbrIdx != endNbrs; ++nbrIdx) {
              const Atom *nbrAtom = mol[*nbrIdx];
              // find if there is a double-bonded nitrogen,
              // or a carbon which is not bonded to other
              // nitrogen atoms with 3 neighbors
              if ((mol.getBondBetweenAtoms(nbrAtom->getIdx(), atom->getIdx()))
                      ->getBondType() == Bond::DOUBLE) {
                doubleBondedCN = ((nbrAtom->getAtomicNum() == 7) ||
                                  (nbrAtom->getAtomicNum() == 6));
                if (nbrAtom->getAtomicNum() == 6) {
                  boost::tie(nbr2Idx, end2Nbrs) = mol.getAtomNeighbors(nbrAtom);
                  for (; doubleBondedCN && (nbr2Idx != end2Nbrs); ++nbr2Idx) {
                    const Atom *nbr2Atom = mol[*nbr2Idx];
                    if (nbr2Atom->getIdx() == atom->getIdx()) {
                      continue;
                    }
                    doubleBondedCN = (!((nbr2Atom->getAtomicNum() == 7) &&
                                        (nbr2Atom->getTotalDegree() == 3)));
                  }
                }
              }
            }
            // if there is a single terminal oxygen
            if (nTermObondedToN == 1) {
              // N2OX
              // sp2-hybridized N-oxide nitrogen
              atomType = 67;
              break;
            }
            // if there are two or more terminal oxygens
            if (nTermObondedToN >= 2) {
              // NO2
              // Nitrogen in nitro group
              // NO3
              // Nitrogen in nitrate group
              atomType = 45;
              break;
            }
            // if the carbon bonded to ipso is bonded to 1 nitrogen
            // with 3 neighbors, that nitrogen is ipso (>N+=C)
            // alternatively, if there is no carbon but ipso is
            // double bonded to nitrogen, we have >N+=N
            if (doubleBondedCN) {
              // N+=C
              // Iminium nitrogen
              // N+=N
              // Positively charged nitrogen doubly bonded to N
              atomType = 54;
              break;
            }
          }
          // total bond order >= 3
          if ((atom->getValence(Atom::ValenceType::EXPLICIT) +
               atom->getNumImplicitHs()) >= 3) {
            bool isNCOorNCS = false;
            bool isNCNplus = false;
            bool isNGDplus = false;
            bool isNNNorNNC = false;
            bool isNbrC = false;
            bool isNbrBenzeneC = false;
            unsigned int elementDoubleBondedToC = 0;
            unsigned int elementTripleBondedToC = 0;
            unsigned int nN2bondedToC = 0;
            unsigned int nN3bondedToC = 0;
            unsigned int nObondedToC = 0;
            unsigned int nSbondedToC = 0;
            // loop over neighbors
            boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
            for (; nbrIdx != endNbrs; ++nbrIdx) {
              const Atom *nbrAtom = mol[*nbrIdx];
              // if the neighbor is carbon
              if (nbrAtom->getAtomicNum() == 6) {
                isNbrC = true;
                // check if we have a benzene carbon close to ipso
                if (nbrAtom->getIsAromatic() &&
                    ringInfo->isAtomInRingOfSize(nbrAtom->getIdx(), 6)) {
                  isNbrBenzeneC = true;
                }
                nN2bondedToC = 0;
                nN3bondedToC = 0;
                nObondedToC = 0;
                nSbondedToC = 0;
                unsigned int nFormalCharge = 0;
                unsigned int nInAromatic6Ring = 0;
                // loop over carbon neighbors
                boost::tie(nbr2Idx, end2Nbrs) = mol.getAtomNeighbors(nbrAtom);
                for (; nbr2Idx != end2Nbrs; ++nbr2Idx) {
                  const Atom *nbr2Atom = mol[*nbr2Idx];
                  const Bond *bond = mol.getBondBetweenAtoms(
                      nbrAtom->getIdx(), nbr2Atom->getIdx());
                  // check if we have oxygen or sulfur double-bonded to this
                  // carbon
                  if ((bond->getBondType() == Bond::DOUBLE) &&
                      ((nbr2Atom->getAtomicNum() == 8) ||
                       (nbr2Atom->getAtomicNum() == 16))) {
                    isNCOorNCS = true;
                  }
                  // check if there is an atom double-bonded to this carbon,
                  // and if so find which element; if it is carbon or
                  // nitrogen (provided that the latter does not belong to
                  // multiple rings), also an aromatic bond is acceptable
                  if ((bond->getBondType() == Bond::DOUBLE) ||
                      ((bond->getBondType() == Bond::AROMATIC) &&
                       ((nbr2Atom->getAtomicNum() == 6) ||
                        ((nbr2Atom->getAtomicNum() == 7) &&
                         (queryIsAtomInNRings(nbr2Atom) == 1))))) {
                    elementDoubleBondedToC = nbr2Atom->getAtomicNum();
                  }
                  // check there is an atom triple-bonded to this carbon,
                  // and if so find which element
                  if (bond->getBondType() == Bond::TRIPLE) {
                    elementTripleBondedToC = nbr2Atom->getAtomicNum();
                  }
                  // if this carbon is bonded to a nitrogen with 3 neighbors
                  if ((nbr2Atom->getAtomicNum() == 7) &&
                      (nbr2Atom->getTotalDegree() == 3)) {
                    // count the number of +1 formal charges that we have
                    if (nbr2Atom->getFormalCharge() == 1) {
                      ++nFormalCharge;
                    }
                    if (rmSize.isAtomInAromaticRingOfSize(nbrAtom, 6)) {
                      ++nInAromatic6Ring;
                    }
                    // count how many oxygens are bonded to this nitrogen
                    // with 3 neighbors
                    unsigned int nObondedToN3 = 0;
                    boost::tie(nbr3Idx, end3Nbrs) =
                        mol.getAtomNeighbors(nbr2Atom);
                    for (; nbr3Idx != end3Nbrs; ++nbr3Idx) {
                      const Atom *nbr3Atom = mol[*nbr3Idx];
                      if (nbr3Atom->getAtomicNum() == 8) {
                        ++nObondedToN3;
                      }
                    }
                    // if there are less than 2 oxygens, this is neither
                    // a nitro group nor a nitrate, so increment the counter
                    // of nitrogens with 3 neighbors bonded to this carbon
                    // (C-N<)
                    if (nObondedToN3 < 2) {
                      ++nN3bondedToC;
                    }
                  }
                  // if this carbon is bonded to a nitrogen with 2 neighbors
                  // via a double or aromatic bond, increment the counter
                  // of nitrogens with 2 neighbors bonded to this carbon
                  // via a double or aromatic bond (C=N-)
                  if ((nbr2Atom->getAtomicNum() == 7) &&
                      (nbr2Atom->getTotalDegree() == 2) &&
                      ((bond->getBondType() == Bond::DOUBLE) ||
                       (bond->getBondType() == Bond::AROMATIC))) {
                    ++nN2bondedToC;
                  }
                  // if this carbon is bonded to an aromatic atom
                  if (nbr2Atom->getIsAromatic()) {
                    // if it is oxygen, increment the counter of
                    // aromatic oxygen atoms bonded to this carbon
                    if (nbr2Atom->getAtomicNum() == 8) {
                      ++nObondedToC;
                    }
                    // if it is sulfur, increment the counter of
                    // aromatic sulfur atoms bonded to this carbon
                    if (nbr2Atom->getAtomicNum() == 16) {
                      ++nSbondedToC;
                    }
                  }
                }
                // if nitrogen is bonded to this carbon via a double or aromatic
                // bond
                if (elementDoubleBondedToC == 7) {
                  // if 2 nitrogens with 3 neighbors and no nitrogens with 2
                  // neighbors
                  // are bonded to this carbon, and we have a formal charge,
                  // but not a 6-membered aromatic ring, and the carbon atom
                  // is not sp3, then this is an amidinium nitrogen (>N-C=N+<)
                  if ((nN3bondedToC == 2) && (!nN2bondedToC) && nFormalCharge &&
                      (!nInAromatic6Ring) && (nbrAtom->getTotalDegree() < 4)) {
                    isNCNplus = true;
                  }
                  // if 3 nitrogens with 3 neighbors are bonded
                  // to this carbon, then this is a guanidinium nitrogen
                  // ((>N-)2-C=N+<)
                  if (nN3bondedToC == 3) {
                    isNGDplus = true;
                  }
                }
              }
              // if the neighbor is nitrogen
              if (nbrAtom->getAtomicNum() == 7) {
                unsigned int nNbondedToN = 0;
                unsigned int nObondedToN = 0;
                unsigned int nSbondedToN = 0;
                // loop over nitrogen neighbors
                boost::tie(nbr2Idx, end2Nbrs) = mol.getAtomNeighbors(nbrAtom);
                for (; nbr2Idx != end2Nbrs; ++nbr2Idx) {
                  const Atom *nbr2Atom = mol[*nbr2Idx];
                  const Bond *bond = mol.getBondBetweenAtoms(
                      nbrAtom->getIdx(), nbr2Atom->getIdx());
                  // if the bond to nitrogen is double
                  if (bond->getBondType() == Bond::DOUBLE) {
                    // if the neighbor is carbon (N=N-C)
                    if (nbr2Atom->getAtomicNum() == 6) {
                      // loop over carbon neighbors
                      boost::tie(nbr3Idx, end3Nbrs) =
                          mol.getAtomNeighbors(nbr2Atom);
                      for (; nbr3Idx != end3Nbrs; ++nbr3Idx) {
                        const Atom *nbr3Atom = mol[*nbr3Idx];
                        // if the nitrogen neighbor to ipso is met, move on
                        if (nbr3Atom->getIdx() == nbrAtom->getIdx()) {
                          continue;
                        }
                        // count how many nitrogen, oxygen, sulfur atoms
                        // are bonded to this carbon
                        switch (nbr3Atom->getAtomicNum()) {
                          case 7:
                            ++nNbondedToN;
                            break;
                          case 8:
                            ++nObondedToN;
                            break;
                          case 16:
                            ++nSbondedToN;
                            break;
                        }
                      }
                      // if there are no more nitrogens, no oxygen, no sulfur
                      // bonded
                      // to carbon, and the latter is not a benzene carbon
                      // then it is N=N-C
                      if ((!nObondedToN) && (!nSbondedToN) && (!nNbondedToN) &&
                          (!isNbrBenzeneC)) {
                        isNNNorNNC = true;
                      }
                    }
                    // if the neighbor is nitrogen (N=N-N) and ipso is not
                    // bonded
                    // to benzene carbon then it is N=N-N
                    if ((nbr2Atom->getAtomicNum() == 7) && (!isNbrBenzeneC)) {
                      isNNNorNNC = true;
                    }
                  }
                }
              }
            }
            // if ipso nitrogen is bonded to carbon
            if (isNbrC) {
              // if neighbor carbon is triple-bonded to N, then ipso is N-C%N
              if (elementTripleBondedToC == 7) {
                isNSO2orNSO3orNCN = true;
              }
              // if neighbor carbon is amidinium
              if (isNCNplus) {
                // NCN+
                // Either nitrogen in N+=C-N
                atomType = 55;
                break;
              }
              // if neighbor carbon is guanidinium
              if (isNGDplus) {
                // NGD+
                // Guanidinium nitrogen
                atomType = 56;
                break;
              }
              // if neighbor carbon is not bonded to oxygen or sulfur
              // and is not cyano, there two possibilities:
              // 1) ipso nitrogen is bonded to benzene carbon while no oxygen
              //    or sulfur are bonded to the latter: ipso is aniline nitrogen
              // 2) ipso nitrogen is bonded to a carbon which is double-bonded
              // to
              //    carbon, nitrogen or phosphorus, or triple-bonded to carbon
              if (((!isNCOorNCS) && (!isNSO2orNSO3orNCN)) &&
                  (((!nObondedToC) && (!nSbondedToC) && isNbrBenzeneC) ||
                   ((elementDoubleBondedToC == 6) ||
                    (elementDoubleBondedToC == 7) ||
                    (elementDoubleBondedToC == 15) ||
                    (elementTripleBondedToC == 6)))) {
                // NC=C
                // Enamine or aniline nitrogen, deloc. lp
                // NC=N
                // Nitrogen in N-C=N with deloc. lp
                // NC=P
                // Nitrogen in N-C=P with deloc. lp
                // NC%C
                // Nitrogen attached to C-C triple bond
                atomType = 40;
                break;
              }
            }
            // if ipso is not sulfonamide while it is either amide/thioamide
            // or >N-N=N-/>N-N=C<
            if ((!isNSO2orNSO3orNCN) && (isNCOorNCS || isNNNorNNC)) {
              // NC=O
              // Amide nitrogen
              // NC=S
              // Thioamide nitrogen
              // NN=C
              // Nitrogen in N-N=C moiety with deloc. lp
              // NN=N
              // Nitrogen in N-N=N moiety with deloc. lp
              atomType = 10;
              break;
            }
          }
        }
        // 2 neighbors
        if (atom->getTotalDegree() == 2) {
          // total bond order = 4
          if ((atom->getValence(Atom::ValenceType::EXPLICIT) +
               atom->getNumImplicitHs()) == 4) {
            // loop over neighbors
            bool isIsonitrile = false;
            boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
            for (; (!isIsonitrile) && (nbrIdx != endNbrs); ++nbrIdx) {
              const Atom *nbrAtom = mol[*nbrIdx];
              // if neighbor is triple-bonded
              isIsonitrile =
                  ((mol.getBondBetweenAtoms(atom->getIdx(), nbrAtom->getIdx()))
                       ->getBondType() == Bond::TRIPLE);
            }
            if (isIsonitrile) {
              // NR%
              // Isonitrile nitrogen
              atomType = 61;
              break;
            }
            // =N=
            // Central nitrogen in C=N=N or N=N=N
            atomType = 53;
            break;
          }
          // total bond order = 3
          if ((atom->getValence(Atom::ValenceType::EXPLICIT) +
               atom->getNumImplicitHs()) == 3) {
            // loop over neighbors
            bool isNitroso = false;
            bool isImineOrAzo = false;
            boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
            for (; nbrIdx != endNbrs; ++nbrIdx) {
              const Atom *nbrAtom = mol[*nbrIdx];
              // if the neighbor is double bonded (-N=)
              if ((mol.getBondBetweenAtoms(atom->getIdx(), nbrAtom->getIdx()))
                      ->getBondType() == Bond::DOUBLE) {
                // if it is terminal oxygen (-N=O)
                isNitroso =
                    ((nbrAtom->getAtomicNum() == 8) && (nTermObondedToN == 1));
                // if it is carbon or nitrogen (-N=N-, -N=C<),
                // ipso is imine or azo
                isImineOrAzo = ((nbrAtom->getAtomicNum() == 6) ||
                                (nbrAtom->getAtomicNum() == 7));
              }
            }
            if (isNitroso && (!isImineOrAzo)) {
              // N=O
              // Nitrogen in nitroso group
              atomType = 46;
              break;
            }
            if (isImineOrAzo) {
              // N=C
              // Imine nitrogen
              // N=N
              // Azo-group nitrogen
              atomType = 9;
              break;
            }
          }
          // total bond order >= 2
          if ((atom->getValence(Atom::ValenceType::EXPLICIT) +
               atom->getNumImplicitHs()) >= 2) {
            // loop over neighbors
            bool isNSO = false;
            boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
            for (; (!isNSO) && (nbrIdx != endNbrs); ++nbrIdx) {
              const Atom *nbrAtom = mol[*nbrIdx];
              // if the neighbor is sulfur bonded to a single terminal oxygen
              if (nbrAtom->getAtomicNum() == 16) {
                // loop over neighbors and count how many
                // terminal oxygens are bonded to sulfur
                unsigned int nTermObondedToS = 0;
                boost::tie(nbr2Idx, end2Nbrs) = mol.getAtomNeighbors(nbrAtom);
                for (; nbr2Idx != end2Nbrs; ++nbr2Idx) {
                  const Atom *nbr2Atom = mol[*nbr2Idx];
                  if ((nbr2Atom->getAtomicNum() == 8) &&
                      (nbr2Atom->getTotalDegree() == 1)) {
                    ++nTermObondedToS;
                  }
                }
                isNSO = (nTermObondedToS == 1);
              }
            }
            if (isNSO) {
              // NSO
              // Divalent nitrogen replacing monovalent O in SO2 group
              atomType = 48;
              break;
            }
            if (!isNSO2orNSO3orNCN) {
              // If it is not sulfonamide deprotonated nitrogen,
              // it is anionic nitrogen (>N::-)
              // NM
              // Anionic divalent nitrogen
              atomType = 62;
              break;
            }
          }
        }
        // if it is sulfonamide (3 neighbors) or cyano (2 neighbors)
        if (isNSO2orNSO3orNCN) {
          // NSO2
          // Sulfonamide nitrogen
          // NSO3
          // Sulfonamide nitrogen
          // NC%N
          // Nitrogen attached to cyano group
          atomType = 43;
          break;
        }
        // 1 neighbor
        if (atom->getTotalDegree() == 1) {
          bool isNSP = false;
          bool isNAZT = false;
          boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
          for (; (!isNSP) && (!isNAZT) && (nbrIdx != endNbrs); ++nbrIdx) {
            const Atom *nbrAtom = mol[*nbrIdx];
            // if ipso is triple-bonded to its only neighbor
            isNSP =
                ((mol.getBondBetweenAtoms(atom->getIdx(), nbrAtom->getIdx()))
                     ->getBondType() == Bond::TRIPLE);
            // ipso is bonded to a nitrogen atom with 2 neighbors
            if ((nbrAtom->getAtomicNum() == 7) &&
                (nbrAtom->getTotalDegree() == 2)) {
              // loop over nitrogen neighbors
              boost::tie(nbr2Idx, end2Nbrs) = mol.getAtomNeighbors(nbrAtom);
              for (; (!isNAZT) && (nbr2Idx != end2Nbrs); ++nbr2Idx) {
                const Atom *nbr2Atom = mol[*nbr2Idx];
                // if another nitrogen with 2 neighbors, or a carbon
                // with 3 neighbors is found, ipso is NAZT
                isNAZT = (((nbr2Atom->getAtomicNum() == 7) &&
                           (nbr2Atom->getTotalDegree() == 2)) ||
                          ((nbr2Atom->getAtomicNum() == 6) &&
                           (nbr2Atom->getTotalDegree() == 3)));
              }
            }
          }
          if (isNSP) {
            // NSP
            // Triply bonded nitrogen
            atomType = 42;
            break;
          }
          if (isNAZT) {
            // NAZT
            // Terminal nitrogen in azido or diazo group
            atomType = 47;
            break;
          }
        }
        // if nothing else was found
        // NR
        // Amine nitrogen
        atomType = 8;
        break;

      // Oxygen
      case 8:
        // 3 neighbors
        if (atom->getTotalDegree() == 3) {
          // O+
          // Oxonium oxygen
          atomType = 49;
          break;
        }
        // 2 neighbors
        if (atom->getTotalDegree() == 2) {
          if ((atom->getValence(Atom::ValenceType::EXPLICIT) +
               atom->getNumImplicitHs()) == 3) {
            // O=+
            // Oxenium oxygen
            atomType = 51;
            break;
          }
          // count how many hydrogens are bound to ipso
          unsigned int nHbondedToO = 0;
          boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
          for (; nbrIdx != endNbrs; ++nbrIdx) {
            const Atom *nbrAtom = mol[*nbrIdx];
            if (nbrAtom->getAtomicNum() == 1) {
              ++nHbondedToO;
            }
          }
          if ((nHbondedToO + atom->getNumImplicitHs()) == 2) {
            // OH2
            // Oxygen in water
            atomType = 70;
            break;
          }
          // otherwise, ipso must be one of the following
          // OC=O
          // Carboxylic acid or ester oxygen
          // OC=C
          // Enolic or phenolic oxygen
          // OC=N
          // Oxygen in -O-C=N- moiety
          // OC=S
          // Divalent oxygen in thioacid or ester
          // ONO2
          // Divalent nitrate "ether" oxygen
          // ON=O
          // Divalent nitrate "ether" oxygen
          // OSO3
          // Divalent oxygen in sulfate group
          // OSO2
          // Divalent oxygen in sulfite group
          // OSO
          // One of two divalent oxygens attached to sulfur
          // OS=O
          // Divalent oxygen in R(RO)S=O
          // -OS
          // Other divalent oxygen attached to sulfur
          // OPO3
          // Divalent oxygen in phosphate group
          // OPO2
          // Divalent oxygen in phosphite group
          // OPO
          // Divalent oxygen, one of two oxygens attached to P
          // -OP
          // Other divalent oxygen attached to phosphorus
          atomType = 6;
          break;
        }
        // 1 neighbor
        if (atom->getDegree() <= 1) {
          unsigned int nNbondedToCorNorS = 0;
          unsigned int nObondedToCorNorS = 0;
          unsigned int nSbondedToCorNorS = 0;
          bool isOxideOBondedToH =
              atom->getNumExplicitHs() + atom->getNumImplicitHs() > 0;
          bool isCarboxylateO = false;
          bool isCarbonylO = false;
          bool isOxideOBondedToC = false;
          bool isNitrosoO = false;
          bool isOxideOBondedToN = false;
          bool isNOxideO = false;
          bool isNitroO = false;
          bool isThioSulfinateO = false;
          bool isSulfateO = false;
          bool isSulfoxideO = false;
          bool isPhosphateOrPerchlorateO = false;
          // loop over neighbors
          boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
          for (; (nbrIdx != endNbrs) && (!isOxideOBondedToC) &&
                 (!isOxideOBondedToN) && (!isOxideOBondedToH) &&
                 (!isCarboxylateO) && (!isNitroO) && (!isNOxideO) &&
                 (!isThioSulfinateO) && (!isSulfateO) &&
                 (!isPhosphateOrPerchlorateO) && (!isCarbonylO) &&
                 (!isNitrosoO) && (!isSulfoxideO);
               ++nbrIdx) {
            const Atom *nbrAtom = mol[*nbrIdx];
            const Bond *bond =
                mol.getBondBetweenAtoms(atom->getIdx(), nbrAtom->getIdx());
            // if the neighbor is carbon, nitrogen or sulfur
            if ((nbrAtom->getAtomicNum() == 6) ||
                (nbrAtom->getAtomicNum() == 7) ||
                (nbrAtom->getAtomicNum() == 16)) {
              // count how many terminal oxygen/sulfur atoms
              // or secondary nitrogens
              // are bonded to the carbon or nitrogen neighbor of ipso
              boost::tie(nbr2Idx, end2Nbrs) = mol.getAtomNeighbors(nbrAtom);
              for (; nbr2Idx != end2Nbrs; ++nbr2Idx) {
                const Atom *nbr2Atom = mol[*nbr2Idx];
                if ((nbr2Atom->getAtomicNum() == 7) &&
                    (nbr2Atom->getTotalDegree() == 2)) {
                  ++nNbondedToCorNorS;
                }
                if ((nbr2Atom->getAtomicNum() == 8) &&
                    (nbr2Atom->getTotalDegree() == 1)) {
                  ++nObondedToCorNorS;
                }
                if ((nbr2Atom->getAtomicNum() == 16) &&
                    (nbr2Atom->getTotalDegree() == 1)) {
                  ++nSbondedToCorNorS;
                }
              }
            }
            // if ipso neighbor is hydrogen
            isOxideOBondedToH = (nbrAtom->getAtomicNum() == 1);

            // if ipso neighbor is carbon
            if (nbrAtom->getAtomicNum() == 6) {
              // if carbon neighbor is bonded to 2 oxygens,
              // ipso is carboxylate oxygen
              isCarboxylateO = (nObondedToCorNorS == 2);
              // if ipso oxygen is bonded to carbon
              // via a double bond, ipso is carbonyl oxygen
              isCarbonylO = (bond->getBondType() == Bond::DOUBLE);
              // if ipso oxygen is bonded to carbon via a
              // single bond, and there are no other bonded oxygens,
              // ipso is oxide oxygen
              isOxideOBondedToC = ((bond->getBondType() == Bond::SINGLE) &&
                                   (nObondedToCorNorS == 1));
            }

            // if ipso neighbor is nitrogen
            if (nbrAtom->getAtomicNum() == 7) {
              // if ipso oxygen is bonded to nitrogen
              // via a double bond, ipso is nitroso oxygen
              isNitrosoO = (bond->getBondType() == Bond::DOUBLE);
              // if ipso oxygen is bonded to nitrogen via a single bond
              // and there are no other bonded oxygens
              if ((bond->getBondType() == Bond::SINGLE) &&
                  (nObondedToCorNorS == 1)) {
                // if nitrogen has 2 neighbors or, if the neighbors are 3,
                // the total bond order on nitrogen is 3, ipso is oxide oxygen
                isOxideOBondedToN =
                    ((nbrAtom->getTotalDegree() == 2) ||
                     ((nbrAtom->getValence(Atom::ValenceType::EXPLICIT) +
                       nbrAtom->getNumImplicitHs()) == 3));
                // if the total bond order on nitrogen is 4, ipso is N-oxide
                // oxygen
                isNOxideO = ((nbrAtom->getValence(Atom::ValenceType::EXPLICIT) +
                              nbrAtom->getNumImplicitHs()) == 4);
              }
              // if ipso oxygen is bonded to nitrogen which is bonded
              // to multiple oxygens, ipso is nitro/nitrate oxygen
              isNitroO = (nObondedToCorNorS >= 2);
            }

            // if ipso neighbor is sulfur
            if (nbrAtom->getAtomicNum() == 16) {
              // if ipso oxygen is bonded to sulfur and
              // the latter is bonded to another sulfur,
              // ipso is thiosulfinate oxygen
              isThioSulfinateO = (nSbondedToCorNorS == 1);
              // if ipso oxygen is bonded to sulfur via a single
              // bond or, if the bond is double, there are multiple
              // oxygen/nitrogen atoms bonded to that sulfur,
              // ipso is sulfate oxygen
              isSulfateO = ((bond->getBondType() == Bond::SINGLE) ||
                            ((bond->getBondType() == Bond::DOUBLE) &&
                             ((nObondedToCorNorS + nNbondedToCorNorS) > 1)));
              // if ipso oxygen is bonded to sulfur via a double
              // bond and the sum of oxygen/nitrogen atoms bonded
              // to that sulfur is 1, ipso is sulfoxide oxygen
              isSulfoxideO = ((bond->getBondType() == Bond::DOUBLE) &&
                              ((nObondedToCorNorS + nNbondedToCorNorS) == 1));
            }

            // if ipso neighbor is phosphorus or chlorine
            isPhosphateOrPerchlorateO = ((nbrAtom->getAtomicNum() == 15) ||
                                         (nbrAtom->getAtomicNum() == 17));
          }
          if (isOxideOBondedToC || isOxideOBondedToN || isOxideOBondedToH) {
            // OM
            // Oxide oxygen on sp3 carbon
            // OM2
            // Oxide oxygen on sp2 carbon
            // OM
            // Oxide oxygen on sp3 nitrogen (not in original MMFF.I Table III)
            // OM2
            // Oxide oxygen on sp2 nitrogen (not in original MMFF.I Table III)
            atomType = 35;
            break;
          }
          if (isCarboxylateO || isNitroO || isNOxideO || isThioSulfinateO ||
              isSulfateO || isPhosphateOrPerchlorateO) {
            // O2CM
            // Oxygen in carboxylate group
            // ONX
            // Oxygen in N-oxides
            // O2N
            // Oxygen in nitro group
            // O2NO
            // Nitro-group oxygen in nitrate
            // O3N
            // Nitrate anion oxygen
            // OSMS
            // Terminal oxygen in thiosulfinate anion
            // O-S
            // Single terminal O on tetracoordinate sulfur
            // O2S
            // One of 2 terminal O's on sulfur
            // O3S
            // One of 3 terminal O's on sulfur
            // O4S
            // Terminal O in sulfate anion
            // OP
            // Oxygen in phosphine oxide
            // O2P
            // One of 2 terminal O's on P
            // O3P
            // One of 3 terminal O's on P
            // O4P
            // One of 4 terminal O's on P
            // O4Cl
            // Oxygen in perchlorate anion
            atomType = 32;
            break;
          }
          if (isCarbonylO || isNitrosoO || isSulfoxideO) {
            // O=C
            // Generic carbonyl oxygen
            // O=CN
            // Carbonyl oxygen in amides
            // O=CR
            // Carbonyl oxygen in aldehydes and ketones
            // O=CO
            // Carbonyl oxygen in acids and esters
            // O=N
            // Nitroso oxygen
            // O=S
            // Doubly bonded sulfoxide oxygen
            atomType = 7;
            break;
          }
        }
        break;

      // Fluorine
      case 9:
        // 1 neighbor
        if (atom->getDegree() == 1) {
          // F
          // Fluorine
          atomType = 11;
          break;
        }
        if (atom->getDegree() == 0) {
          // F-
          // Fluoride anion
          atomType = 89;
          break;
        }
        break;

      // Sodium
      case 11:
        if (atom->getDegree() == 0) {
          // NA+
          // Sodium cation
          atomType = 93;
          break;
        }
        break;

      // Magnesium
      case 12:
        if (atom->getDegree() == 0) {
          // MG+2
          // Dipositive magnesium cation
          atomType = 99;
          break;
        }
        break;

      // Silicon
      case 14:
        // SI
        // Silicon
        atomType = 19;
        break;

      // Phosphorus
      case 15:
        if (atom->getTotalDegree() == 4) {
          // PO4
          // Phosphate group phosphorus
          // PO3
          // Phosphorus with 3 attached oxygens
          // PO2
          // Phosphorus with 2 attached oxygens
          // PO
          // Phosphine oxide phosphorus
          // PTET
          // General tetracoordinate phosphorus
          atomType = 25;
          break;
        }
        if (atom->getTotalDegree() == 3) {
          // P
          // Phosphorus in phosphines
          atomType = 26;
          break;
        }
        if (atom->getTotalDegree() == 2) {
          // -P=C
          // Phosphorus doubly bonded to C
          atomType = 75;
          break;
        }
        break;

      // Sulfur
      case 16:
        // 3  or 4 neighbors
        if ((atom->getTotalDegree() == 3) || (atom->getTotalDegree() == 4)) {
          unsigned int nOorNbondedToS = 0;
          unsigned int nSbondedToS = 0;
          bool isCDoubleBondedToS = false;
          // loop over neighbors
          boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
          for (; nbrIdx != endNbrs; ++nbrIdx) {
            const Atom *nbrAtom = mol[*nbrIdx];
            // check if ipso sulfur is double-bonded to carbon
            if ((nbrAtom->getAtomicNum() == 6) &&
                ((mol.getBondBetweenAtoms(atom->getIdx(), nbrAtom->getIdx()))
                     ->getBondType() == Bond::DOUBLE)) {
              isCDoubleBondedToS = true;
            }
            // if the neighbor is terminal oxygen/sulfur
            // or secondary nitrogen, increment the respective counter
            if (((nbrAtom->getDegree() == 1) &&
                 (nbrAtom->getAtomicNum() == 8)) ||
                ((nbrAtom->getTotalDegree() == 2) &&
                 (nbrAtom->getAtomicNum() == 7))) {
              ++nOorNbondedToS;
            }
            if ((nbrAtom->getDegree() == 1) &&
                (nbrAtom->getAtomicNum() == 16)) {
              ++nSbondedToS;
            }
          }
          // if ipso sulfur has 3 neighbors and is bonded to
          // two atoms of oxygen/nitrogen and double-bonded
          // to carbon, or if it has 4 neighbors
          if (((atom->getTotalDegree() == 3) && (nOorNbondedToS == 2) &&
               (isCDoubleBondedToS)) ||
              (atom->getTotalDegree() == 4)) {
            // =SO2
            // Sulfone sulfur, doubly bonded to carbon
            atomType = 18;
            break;
          }
          // if ipso sulfur is bonded to both oxygen/nitrogen and sulfur
          if ((nOorNbondedToS && nSbondedToS) ||
              ((nOorNbondedToS == 2) && (!isCDoubleBondedToS))) {
            // SSOM
            // Tricoordinate sulfur in anionic thiosulfinate group
            atomType = 73;
            break;
          }
          // otherwise ipso sulfur is double bonded to oxygen or nitrogen
          // S=O
          // Sulfoxide sulfur
          // >S=N
          // Tricoordinate sulfur doubly bonded to N
          atomType = 17;
          break;
        }
        // 2 neighbors
        if (atom->getTotalDegree() == 2) {
          // loop over neighbors
          bool isODoubleBondedToS = false;
          boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
          for (; nbrIdx != endNbrs; ++nbrIdx) {
            const Atom *nbrAtom = mol[*nbrIdx];
            // check if ipso sulfur is double-bonded to oxygen
            if ((nbrAtom->getAtomicNum() == 8) &&
                ((mol.getBondBetweenAtoms(atom->getIdx(), nbrAtom->getIdx()))
                     ->getBondType() == Bond::DOUBLE)) {
              isODoubleBondedToS = true;
            }
          }
          // if ipso sulfur is double-bonded to oxygen
          if (isODoubleBondedToS) {
            // =S=O
            // Sulfinyl sulfur, e.g., in C=S=O
            atomType = 74;
            break;
          }
          // otherwise it is a thiol, sulfide or disulfide
          // S
          // Thiol, sulfide, or disulfide sulfur
          atomType = 15;
          break;
        }
        // 1 neighbor
        if (atom->getDegree() == 1) {
          unsigned int nTermSbondedToNbr = 0;
          bool isCDoubleBondedToS = false;
          // find the neighbor and count how many terminal sulfur
          // atoms are there, including ipso
          boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
          for (; nbrIdx != endNbrs; ++nbrIdx) {
            const Atom *nbrAtom = mol[*nbrIdx];
            boost::tie(nbr2Idx, end2Nbrs) = mol.getAtomNeighbors(nbrAtom);
            for (; nbr2Idx != end2Nbrs; ++nbr2Idx) {
              const Atom *nbr2Atom = mol[*nbr2Idx];
              if ((nbr2Atom->getAtomicNum() == 16) &&
                  (nbr2Atom->getTotalDegree() == 1)) {
                ++nTermSbondedToNbr;
              }
            }
            // check if ipso sulfur is double-bonded to carbon
            if ((nbrAtom->getAtomicNum() == 6) &&
                ((mol.getBondBetweenAtoms(atom->getIdx(), nbrAtom->getIdx()))
                     ->getBondType() == Bond::DOUBLE)) {
              isCDoubleBondedToS = true;
            }
          }
          // if ipso sulfur is double bonded to carbon and the latter
          // is not bonded to other terminal sulfur atoms, then it is
          // not a dithiocarboxylate, but a thioketone, etc.
          if (isCDoubleBondedToS && (nTermSbondedToNbr != 2)) {
            // S=C
            // Sulfur doubly bonded to carbon
            atomType = 16;
            break;
          }
          // otherwise ipso must be one of these
          // S-P
          // Terminal sulfur bonded to P
          // SM
          // Anionic terminal sulfur
          // SSMO
          // Terminal sulfur in thiosulfinate group
          atomType = 72;
          break;
        }
        break;

      // Chlorine
      case 17:
        // 4 neighbors
        if (atom->getTotalDegree() == 4) {
          // loop over neighbors and count the number
          // of bonded oxygens
          unsigned int nObondedToCl = 0;
          boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
          for (; nbrIdx != endNbrs; ++nbrIdx) {
            const Atom *nbrAtom = mol[*nbrIdx];
            if (nbrAtom->getAtomicNum() == 8) {
              ++nObondedToCl;
            }
          }
          // if there are 4 oxygens
          if (nObondedToCl == 4) {
            // CLO4
            // Perchlorate anione chlorine
            atomType = 77;
            break;
          }
        }
        // 1 neighbor
        if (atom->getTotalDegree() == 1) {
          // Cl
          // Chlorine
          atomType = 12;
          break;
        }
        // 0 neighbors
        if (atom->getDegree() == 0) {
          // Cl-
          // Chloride anion
          atomType = 90;
          break;
        }
        break;

      // Potassium
      case 19:
        if (atom->getDegree() == 0) {
          // K+
          // Potassium cation
          atomType = 94;
          break;
        }
        break;

      // Calcium
      case 20:
        if (atom->getDegree() == 0) {
          // CA+2
          // Dipositive calcium cation
          atomType = 96;
          break;
        }
        break;

      // Iron
      case 26:
        if (atom->getDegree() == 0) {
          if (atom->getFormalCharge() == 2) {
            // FE+2
            // Dipositive iron cation
            atomType = 87;
            break;
          }
          if (atom->getFormalCharge() == 3) {
            // FE+3
            // Tripositive iron cation
            atomType = 88;
            break;
          }
        }
        break;

      // Copper
      case 29:
        if (atom->getDegree() == 0) {
          if (atom->getFormalCharge() == 1) {
            // CU+1
            // Monopositive copper cation
            atomType = 97;
            break;
          }
          if (atom->getFormalCharge() == 2) {
            // CU+2
            // Dipositive copper cation
            atomType = 98;
            break;
          }
        }
        break;

      // Zinc
      case 30:
        if (atom->getDegree() == 0) {
          // ZN+2
          // Dipositive zinc cation
          atomType = 95;
          break;
        }
        break;

      // Bromine
      case 35:
        if (atom->getDegree() == 1) {
          // Br
          // Bromine
          atomType = 13;
          break;
        }
        if (atom->getDegree() == 0) {
          // BR-
          // Bromide anion
          atomType = 91;
          break;
        }
        break;

      // Iodine
      case 53:
        if (atom->getDegree() == 1) {
          // I
          // Iodine
          atomType = 14;
          break;
        }
        break;
    }
  }
  d_MMFFAtomPropertiesPtrVect[atom->getIdx()]->mmffAtomType = atomType;
  if (!atomType) {
    d_valid = false;
  }
}

// finds the MMFF atomType for a hydrogen atom
void MMFFMolProperties::setMMFFHydrogenType(const Atom *atom) {
  unsigned int atomType = 0;
  bool isHOCCorHOCN = false;
  bool isHOCO = false;
  bool isHOP = false;
  bool isHOS = false;
  const ROMol &mol = atom->getOwningMol();
  ROMol::ADJ_ITER nbrIdx;
  ROMol::ADJ_ITER endNbrs;
  ROMol::ADJ_ITER nbr2Idx;
  ROMol::ADJ_ITER end2Nbrs;
  ROMol::ADJ_ITER nbr3Idx;
  ROMol::ADJ_ITER end3Nbrs;

  // loop over neighbors (actually there can be only one)
  boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
  for (; nbrIdx != endNbrs; ++nbrIdx) {
    const Atom *nbrAtom = mol[*nbrIdx];
    switch (nbrAtom->getAtomicNum()) {
      // carbon, silicon
      case 6:
      case 14:
        // HC
        // Hydrogen attached to carbon
        // HSI
        // Hydrogen attached to silicon
        atomType = 5;
        break;

      // nitrogen
      case 7:
        switch (this->getMMFFAtomType(nbrAtom->getIdx())) {
          case 8:
          // HNR
          // Generic hydrogen on sp3 nitrogen, e.g. in amines
          // H3N
          // Hydrogen in ammonia
          case 39:
          // HPYL
          // Hydrogen on nitrogen in pyrrole
          case 62:
          // HNR
          // Generic hydrogen on sp3 nitrogen, e.g. in amines
          case 67:
          case 68:
            // HNOX
            // Hydrogen on N in a N-oxide
            atomType = 23;
            break;

          case 34:
          // NR+
          // Quaternary nitrogen
          case 54:
          // N+=C
          // Iminium nitrogen
          // N+=N
          // Positively charged nitrogen doubly bonded to N
          case 55:
          // HNN+
          // Hydrogen on amidinium nitrogen
          case 56:
          // HGD+
          // Hydrogen on guanidinium nitrogen
          case 58:
          // NPD+
          // Aromatic nitrogen in pyridinium
          case 81:
            // HIM+
            // Hydrogen on imidazolium nitrogen
            atomType = 36;
            break;

          case 9:
            // HN=N
            // Hydrogen on azo nitrogen
            // HN=C
            // Hydrogen on imine nitrogen
            atomType = 27;
            break;

          default:
            // HNCC
            // Hydrogen on enamine nitrogen
            // HNCN
            // Hydrogen in H-N-C=N moiety
            // HNCO
            // Hydrogen on amide nitrogen
            // HNCS
            // Hydrogen on thioamide nitrogen
            // HNNC
            // Hydrogen in H-N-N=C moiety
            // HNNN
            // Hydrogen in H-N-N=N moiety
            // HNSO
            // Hydrogen on NSO, NSO2, or NSO3 nitrogen
            // HNC%
            // Hydrogen on N triply bonded to C
            // HSP2
            // Generic hydrogen on sp2 nitrogen
            atomType = 28;
            break;
        }
        break;

      // oxygen
      case 8:
        switch (this->getMMFFAtomType(nbrAtom->getIdx())) {
          case 49:
            // HO+
            // Hydrogen on oxonium oxygen
            atomType = 50;
            break;

          case 51:
            // HO=+
            // Hydrogen on oxenium oxygen
            atomType = 52;
            break;

          case 70:
            // HOH
            // Hydroxyl hydrogen in water
            atomType = 31;
            break;

          case 6:
            // for hydrogen bonded to atomType 6 oxygen we need to distinguish
            // among acidic hydrogens belonging to carboxylic/phospho acids,
            // enolic/phenolic/hydroxamic hydrogens and hydrogens whose oxygen
            // partner is bonded to sulfur. If none of these is found
            // it is either an alcohol or a generic hydroxyl hydrogen
            // loop over oxygen neighbors
            boost::tie(nbr2Idx, end2Nbrs) = mol.getAtomNeighbors(nbrAtom);
            for (; nbr2Idx != end2Nbrs; ++nbr2Idx) {
              const Atom *nbr2Atom = mol[*nbr2Idx];
              // if the neighbor of oxygen is carbon, loop over the carbon
              // neighbors
              if (nbr2Atom->getAtomicNum() == 6) {
                boost::tie(nbr3Idx, end3Nbrs) = mol.getAtomNeighbors(nbr2Atom);
                for (; nbr3Idx != end3Nbrs; ++nbr3Idx) {
                  const Atom *nbr3Atom = mol[*nbr3Idx];
                  const Bond *bond = mol.getBondBetweenAtoms(
                      nbr2Atom->getIdx(), nbr3Atom->getIdx());
                  // if the starting oxygen is met, move on
                  if (nbr3Atom->getIdx() == nbrAtom->getIdx()) {
                    continue;
                  }
                  // if the carbon neighbor is another carbon or nitrogen
                  // bonded via a double or aromatic bond, ipso is HOCC/HOCN
                  if (((nbr3Atom->getAtomicNum() == 6) ||
                       (nbr3Atom->getAtomicNum() == 7)) &&
                      ((bond->getBondType() == Bond::DOUBLE) ||
                       (bond->getBondType() == Bond::AROMATIC))) {
                    isHOCCorHOCN = true;
                  }
                  // if the carbon neighbor is an oxygen bonded
                  // via a double bond, ipso is HOCO
                  if ((nbr3Atom->getAtomicNum() == 8) &&
                      (bond->getBondType() == Bond::DOUBLE)) {
                    isHOCO = true;
                  }
                }
              }
              // if the neighbor of oxygen is phosphorus, ipso is HOCO
              if (nbr2Atom->getAtomicNum() == 15) {
                isHOP = true;
              }
              // if the neighbor of oxygen is sulfur, ipso is HOS
              if (nbr2Atom->getAtomicNum() == 16) {
                isHOS = true;
              }
            }
            if (isHOCO || isHOP) {
              // HOCO
              // Hydroxyl hydrogen in carboxylic acids
              atomType = 24;
              break;
            }
            if (isHOCCorHOCN) {
              // HOCC
              // Enolic or phenolic hydroxyl hydrogen
              // HOCN
              // Hydroxyl hydrogen in HO-C=N moiety
              atomType = 29;
              break;
            }
            if (isHOS) {
              // HOS
              // Hydrogen on oxygen attached to sulfur
              atomType = 33;
              break;
            }
            /* FALLTHRU */
          default:
            // HO
            // Generic hydroxyl hydrogen
            // HOR
            // Hydroxyl hydrogen in alcohols
            atomType = 21;
            break;
        }
        break;

      // phosphorus and sulfur
      case 15:
      case 16:
        // HP
        // Hydrogen attached to phosphorus
        // HS
        // Hydrogen attached to sulfur
        // HS=N
        // Hydrogen attached to >S= sulfur doubly bonded to N
        atomType = 71;
        break;
    }
  }
  d_MMFFAtomPropertiesPtrVect[atom->getIdx()]->mmffAtomType = atomType;
  if (!atomType) {
    d_valid = false;
  }
}

// sanitizes molecule according to MMFF requirements
// returns MolOps::SANITIZE_NONE on success, the flag
// which caused trouble in case of failure
unsigned int sanitizeMMFFMol(RWMol &mol) {
  unsigned int error = 0;

  try {
    MolOps::sanitizeMol(
        mol, error,
        (unsigned int)(MolOps::SANITIZE_CLEANUP | MolOps::SANITIZE_PROPERTIES |
                       MolOps::SANITIZE_SYMMRINGS | MolOps::SANITIZE_KEKULIZE |
                       MolOps::SANITIZE_FINDRADICALS |
                       MolOps::SANITIZE_SETCONJUGATION |
                       MolOps::SANITIZE_SETHYBRIDIZATION |
                       MolOps::SANITIZE_CLEANUPCHIRALITY |
                       MolOps::SANITIZE_ADJUSTHS));
    if (!(mol.hasProp(common_properties::_MMFFSanitized))) {
      mol.setProp(common_properties::_MMFFSanitized, 1, true);
    }
  } catch (MolSanitizeException &) {
  }

  return error;
}

// constructs a MMFFMolProperties object for ROMol mol filled
// with MMFF atom types, formal and partial charges
// in case atom types are missing, d_valid is set to false,
// charges are set to 0.0 and the force-field is unusable
MMFFMolProperties::MMFFMolProperties(ROMol &mol, const std::string &mmffVariant,
                                     std::uint8_t verbosity,
                                     std::ostream &oStream)
    : d_valid(true),
      d_mmffs(mmffVariant == "MMFF94s"),
      d_bondTerm(true),
      d_angleTerm(true),
      d_stretchBendTerm(true),
      d_oopTerm(true),
      d_torsionTerm(true),
      d_vdWTerm(true),
      d_eleTerm(true),
      d_dielConst(1.0),
      d_dielModel(CONSTANT),
      d_verbosity(verbosity),
      d_oStream(&oStream),
      d_MMFFAtomPropertiesPtrVect(mol.getNumAtoms()) {
  if (MolOps::needsHs(mol)) {
    BOOST_LOG(rdWarningLog)
        << "Molecule does not have explicit Hs. Consider calling AddHs()"
        << std::endl;
  }
  if (!mol.hasProp(common_properties::_MMFFSanitized)) {
    bool isAromaticSet = false;
    for (const auto atom : mol.atoms()) {
      if (atom->getIsAromatic()) {
        isAromaticSet = true;
        break;
      }
    }
    if (isAromaticSet) {
      MolOps::Kekulize((RWMol &)mol, true);
    }
    mol.setProp(common_properties::_MMFFSanitized, 1, true);
  }
  for (unsigned int i = 0; i < mol.getNumAtoms(); ++i) {
    d_MMFFAtomPropertiesPtrVect[i] =
        MMFFAtomPropertiesPtr(new MMFFAtomProperties());
  }
  MolOps::setMMFFAromaticity((RWMol &)mol);
  RingMembershipSize rmSize(mol);
  for (const auto atom : mol.atoms()) {
    if (atom->getAtomicNum() != 1) {
      this->setMMFFHeavyAtomType(rmSize, atom);
    }
  }
  for (const auto atom : mol.atoms()) {
    if (atom->getAtomicNum() == 1) {
      this->setMMFFHydrogenType(atom);
    }
  }
  if (this->isValid()) {
    this->computeMMFFCharges(mol);
  }
  if (verbosity == MMFF_VERBOSITY_HIGH) {
    oStream << "\n"
               "A T O M   T Y P E S   A N D   C H A R G E S\n\n"
               "          ATOM    FORMAL   PARTIAL\n"
               " ATOM     TYPE    CHARGE    CHARGE\n"
               "-----------------------------------"
            << std::endl;
    for (unsigned int idx = 0; idx < mol.getNumAtoms(); ++idx) {
      oStream << std::left << std::setw(2)
              << mol.getAtomWithIdx(idx)->getSymbol() << std::left << " #"
              << std::setw(5) << idx + 1 << std::right << std::setw(5)
              << (unsigned int)(this->getMMFFAtomType(idx)) << std::right
              << std::setw(10) << std::fixed << std::setprecision(3)
              << this->getMMFFFormalCharge(idx) << std::right << std::setw(10)
              << this->getMMFFPartialCharge(idx) << std::endl;
    }
    if (!(this->isValid())) {
      oStream << "\nMissing atom types - charges were not computed"
              << std::endl;
    }
  }
}

// returns the MMFF angle type of the angle formed
// by atoms with indexes idx1, idx2, idx3
unsigned int MMFFMolProperties::getMMFFAngleType(const ROMol &mol,
                                                 const unsigned int idx1,
                                                 const unsigned int idx2,
                                                 const unsigned int idx3) const {
  PRECONDITION(this->isValid(), "missing atom types - invalid force-field");

  // ftp://ftp.wiley.com/public/journals/jcc/suppmat/17/553/MMFF-III_AppendixA.html
  //
  // AT[IJK]    Structural significance
  //--------------------------------------------------------------------------
  //  0		      The angle i-j-k is a "normal" bond angle
  //  1 		    Either bond i-j or bond j-k has a bond type of 1
  //  2		      Bonds i-j and j-k each have bond types of 1; the
  //  sum is 2. 3		      The angle occurs in a three-membered ring
  //  4		      The angle occurs in a four-membered ring
  //  5		      Is in a three-membered ring and the sum of the
  //  bond types is
  //  1
  //  6		      Is in a three-membered ring and the sum of the
  //  bond types is
  //  2
  //  7		      Is in a four-membered ring and the sum of the bond
  //  types is
  //  1
  //  8		      Is in a four-membered ring and the sum of the bond
  //  types is
  //  2

  unsigned int bondTypeSum =
      this->getMMFFBondType(mol.getBondBetweenAtoms(idx1, idx2)) +
      this->getMMFFBondType(mol.getBondBetweenAtoms(idx2, idx3));
  unsigned int angleType = bondTypeSum;

  unsigned int size = isAngleInRingOfSize3or4(mol, idx1, idx2, idx3);
  if (size) {
    angleType = size;
    if (bondTypeSum) {
      angleType += (bondTypeSum + size - 2);
    }
  }

  return angleType;
}

// returns the MMFF bond type of the bond
unsigned int MMFFMolProperties::getMMFFBondType(const Bond *bond) const {
  PRECONDITION(this->isValid(), "missing atom types - invalid force-field");
  PRECONDITION(bond, "invalid bond");

  const MMFFPropCollection *mmffProp = DefaultParameters::getMMFFProp();
  const ForceFields::MMFF::MMFFProp *mmffPropAtom1 =
      (*mmffProp)(this->getMMFFAtomType(bond->getBeginAtomIdx()));
  const ForceFields::MMFF::MMFFProp *mmffPropAtom2 =
      (*mmffProp)(this->getMMFFAtomType(bond->getEndAtomIdx()));

  // return 1 if the bond is single and the properties for this
  // single bond match either those of sbmb or aromatic bonds
  // for this atom pair, 0 if they don't
  return (unsigned int)(((bond->getBondType() == Bond::SINGLE) &&
                         ((mmffPropAtom1->sbmb && mmffPropAtom2->sbmb) ||
                          (mmffPropAtom1->arom && mmffPropAtom2->arom)))
                            ? 1
                            : 0);
}

// given the angle type and the two bond types of the bond
// which compose the angle, it returns the MMFF stretch-bend
// type of the angle
unsigned int getMMFFStretchBendType(const unsigned int angleType,
                                    const unsigned int bondType1,
                                    const unsigned int bondType2) {
  unsigned int stretchBendType = 0;

  switch (angleType) {
    case 1:
      stretchBendType = ((bondType1 || (bondType1 == bondType2)) ? 1 : 2);
      break;

    case 2:
      stretchBendType = 3;
      break;

    case 4:
      stretchBendType = 4;
      break;

    case 3:
      stretchBendType = 5;
      break;

    case 5:
      stretchBendType = ((bondType1 || (bondType1 == bondType2)) ? 6 : 7);
      break;

    case 6:
      stretchBendType = 8;
      break;

    case 7:
      stretchBendType = ((bondType1 || (bondType1 == bondType2)) ? 9 : 10);
      break;

    case 8:
      stretchBendType = 11;
      break;
  }

  return stretchBendType;
}

// given a dihedral angle formed by 4 atoms with indexes
// idx1, idx2, idx3, idx4, it returns a std::pair whose first element
// is the principal torsion type, and the second is the secondary
// torsion type, to be used only if parameters could not be found
// (empirically found - this is not mentioned either in MMFF.IV
// nor in MMFF.V)
const std::pair<unsigned int, unsigned int>
MMFFMolProperties::getMMFFTorsionType(const ROMol &mol, const unsigned int idx1,
                                      const unsigned int idx2,
                                      const unsigned int idx3,
                                      const unsigned int idx4) const {
  PRECONDITION(this->isValid(), "missing atom types - invalid force-field");

  const Bond *bondJK = mol.getBondBetweenAtoms(idx2, idx3);
  unsigned int bondTypeIJ =
      this->getMMFFBondType(mol.getBondBetweenAtoms(idx1, idx2));
  unsigned int bondTypeJK = this->getMMFFBondType(bondJK);
  unsigned int bondTypeKL =
      this->getMMFFBondType(mol.getBondBetweenAtoms(idx3, idx4));
  unsigned int torsionType = bondTypeJK;
  unsigned int secondTorsionType = 0;

  // according to MMFF.IV page 609 the condition should be as simple as
  // if ((bondTypeJK == 0) && ((bondTypeIJ == 1) || (bondTypeKL == 1))) {
  // but CYGUAN01 fails the test, so the following condition was
  // empirically determined to be the correct one
  if ((bondTypeJK == 0) && (bondJK->getBondType() == Bond::SINGLE) &&
      ((bondTypeIJ == 1) || (bondTypeKL == 1))) {
    torsionType = 2;
  }
  unsigned int size = isTorsionInRingOfSize4or5(mol, idx1, idx2, idx3, idx4);
  // the additional check on the existence of a bond between I and K or J and
  // L is to avoid assigning torsionType 4 to those torsions in a 4-membered
  // ring constituted by the fusion of two 3-membered rings, even though it
  // would be harmless for the energy calculation since parameters for
  // 4,22,22,22,22 and 0,22,22,22,22 are identical
  if ((size == 4) && (!(mol.getBondBetweenAtoms(idx1, idx3) ||
                        mol.getBondBetweenAtoms(idx2, idx4)))) {
    secondTorsionType = torsionType;
    torsionType = 4;
  } else if ((size == 5) && ((this->getMMFFAtomType(idx1) == 1) ||
                             (this->getMMFFAtomType(idx2) == 1) ||
                             (this->getMMFFAtomType(idx3) == 1) ||
                             (this->getMMFFAtomType(idx4) == 1))) {
    secondTorsionType = torsionType;
    torsionType = 5;
  }

  return std::make_pair(torsionType, secondTorsionType);
}

// empirical rule to compute bond stretching parameters if
// tabulated parameters could not be found. The returned
// pointer to a MMFFBond object must be freed by the caller
const ForceFields::MMFF::MMFFBond *
MMFFMolProperties::getMMFFBondStretchEmpiricalRuleParams(const ROMol &,
                                                         const Bond *bond) const {
  PRECONDITION(this->isValid(), "missing atom types - invalid force-field");

  const MMFFBond *mmffBndkParams;
  const MMFFHerschbachLaurie *mmffHerschbachLaurieParams;
  const MMFFProp *mmffAtomPropParams[2];
  const MMFFCovRadPauEle *mmffAtomCovRadPauEleParams[2];
  const MMFFBndkCollection *mmffBndk = DefaultParameters::getMMFFBndk();
  const MMFFHerschbachLaurieCollection *mmffHerschbachLaurie =
      DefaultParameters::getMMFFHerschbachLaurie();
  const MMFFCovRadPauEleCollection *mmffCovRadPauEle =
      DefaultParameters::getMMFFCovRadPauEle();
  const MMFFPropCollection *mmffProp = DefaultParameters::getMMFFProp();

  unsigned int atomicNum1 = bond->getBeginAtom()->getAtomicNum();
  unsigned int atomicNum2 = bond->getEndAtom()->getAtomicNum();
  mmffBndkParams = (*mmffBndk)(atomicNum1, atomicNum2);
  mmffAtomCovRadPauEleParams[0] = (*mmffCovRadPauEle)(atomicNum1);
  mmffAtomCovRadPauEleParams[1] = (*mmffCovRadPauEle)(atomicNum2);
  mmffAtomPropParams[0] =
      (*mmffProp)(this->getMMFFAtomType(bond->getBeginAtomIdx()));
  mmffAtomPropParams[1] =
      (*mmffProp)(this->getMMFFAtomType(bond->getEndAtomIdx()));

  PRECONDITION(mmffAtomCovRadPauEleParams[0],
               "covalent radius/Pauling electronegativity parameters for atom "
               "1 not found");
  PRECONDITION(mmffAtomCovRadPauEleParams[1],
               "covalent radius/Pauling electronegativity parameters for atom "
               "2 not found");
  PRECONDITION(mmffAtomPropParams[0],
               "property parameters for atom 1 not found");
  PRECONDITION(mmffAtomPropParams[1],
               "property parameters for atom 2 not found");

  auto *mmffBondParams = new ForceFields::MMFF::MMFFBond();
  const double c = (((atomicNum1 == 1) || (atomicNum2 == 1)) ? 0.050 : 0.085);
  const double n = 1.4;
#if 0
      const double delta = 0.008;
#endif
#if 1
  const double delta = 0.0;
#endif
  double r0_i[2];

  // MMFF.V, page 625
  for (unsigned int i = 0; i < 2; ++i) {
    r0_i[i] = mmffAtomCovRadPauEleParams[i]->r0;
// the part of the empirical rule concerning H
// parameters appears not to be used - tests are
// passed only in its absence, hence it is
// currently excluded
#if 0
        switch (mmffAtomPropParams[i]->mltb) {
          case 1:
          case 2:
            H_i[i] = 2;
          break;
          
          case 3:
            H_i[i] = 1;
          
          default:
            H_i[i] = 3;
        }
#endif
  }
// also the part of the empirical rule concerning BO
// parameters appears not to be used - tests are
// passed only in its absence, hence it is
// currently excluded
#if 0
      unsigned int BO_ij = (unsigned int)(bond->getBondTypeAsDouble());
      if ((mmffAtomPropParams[0]->mltb == 1)
        && (mmffAtomPropParams[1]->mltb == 1)) {
        BO_ij = 4;
      }
      if (((mmffAtomPropParams[0]->mltb == 1)
        && (mmffAtomPropParams[1]->mltb == 2))
        || ((mmffAtomPropParams[0]->mltb == 2)
        && (mmffAtomPropParams[1]->mltb == 1))) {
        BO_ij = 5;
      }
      if (areAtomsInSameAromaticRing(mol,
        bond->getBeginAtomIdx(), bond->getEndAtomIdx())) {
        BO_ij = (((mmffAtomPropParams[0]->pilp == 0)
          && (mmffAtomPropParams[1]->pilp == 0)) ? 4 : 5);
      }
      if (BO_ij == 1) {
        for (unsigned int i = 0; i < 2; ++i) {
          std::cout << "H" << i << "=" << H_i[i] << std::endl;
          switch (H_i[i]) {
            case 1:
              r0_i[i] -= 0.08;
            break;
            
            case 2:
              r0_i[i] -= 0.03;
            break;
          }
        }
      }
      else {
        double dec = 0.0;
        switch (BO_ij) {
          case 5:
            dec = 0.04;
          break;
          
          case 4:
            dec = 0.075;
          break;
          
          case 3:
            dec = 0.17;
          break;
          
          case 2:
            dec = 0.10;
          break;
        }
        r0_i[0] -= dec;
        r0_i[1] -= dec;
      }
#endif
  // equation (18) - MMFF.V, page 625
  mmffBondParams->r0 = (r0_i[0] + r0_i[1] -
                        c * pow(fabs(mmffAtomCovRadPauEleParams[0]->chi -
                                     mmffAtomCovRadPauEleParams[1]->chi),
                                n) -
                        delta);
  if (mmffBndkParams) {
    // equation (19) - MMFF.V, page 625
    double coeff = mmffBndkParams->r0 / mmffBondParams->r0;
    double coeff2 = coeff * coeff;
    double coeff6 = coeff2 * coeff2 * coeff2;
    mmffBondParams->kb = mmffBndkParams->kb * coeff6;
  } else {
    // MMFF.V, page 627
    // Herschbach-Laurie version of Badger's rule
    // J. Chem. Phys. 35, 458 (1961); https://doi.org/10.1063/1.1731952
    // equation (8), page 5
    mmffHerschbachLaurieParams = (*mmffHerschbachLaurie)(
        getPeriodicTableRowHL(atomicNum1), getPeriodicTableRowHL(atomicNum2));
    mmffBondParams->kb =
        pow(10.0, -(mmffBondParams->r0 - mmffHerschbachLaurieParams->a_ij) /
                      mmffHerschbachLaurieParams->d_ij);
  }

  return (const ForceFields::MMFF::MMFFBond *)mmffBondParams;
}

// empirical rule to compute angle bending parameters if
// tabulated parameters could not be found. The returned
// pointer to a MMFFAngle object must be freed by the caller
const ForceFields::MMFF::MMFFAngle *getMMFFAngleBendEmpiricalRuleParams(
    const ROMol &mol, const ForceFields::MMFF::MMFFAngle *oldMMFFAngleParams,
    const ForceFields::MMFF::MMFFProp *mmffPropParamsCentralAtom,
    const ForceFields::MMFF::MMFFBond *mmffBondParams1,
    const ForceFields::MMFF::MMFFBond *mmffBondParams2, unsigned int idx1,
    unsigned int idx2, unsigned int idx3) {
  int atomicNum[3];
  atomicNum[0] = mol.getAtomWithIdx(idx1)->getAtomicNum();
  atomicNum[1] = mol.getAtomWithIdx(idx2)->getAtomicNum();
  atomicNum[2] = mol.getAtomWithIdx(idx3)->getAtomicNum();
  auto *mmffAngleParams = new ForceFields::MMFF::MMFFAngle();
  unsigned int ringSize = isAngleInRingOfSize3or4(mol, idx1, idx2, idx3);
  if (!oldMMFFAngleParams) {
    // angle rest value empirical rule
    mmffAngleParams->theta0 = 120.0;
    switch (mmffPropParamsCentralAtom->crd) {
      case 4:
        // if the central atom has crd = 4
        mmffAngleParams->theta0 = 109.45;
        break;

      case 2:
        // if the central atom is oxygen
        if (atomicNum[1] == 8) {
          mmffAngleParams->theta0 = 105.0;
        }
        // if the central atom is linear
        else if (mmffPropParamsCentralAtom->linh == 1) {
          mmffAngleParams->theta0 = 180.0;
        }
        break;

      case 3:
        if ((mmffPropParamsCentralAtom->val == 3) &&
            (mmffPropParamsCentralAtom->mltb == 0)) {
          // if the central atom is nitrogen
          if (atomicNum[1] == 7) {
            mmffAngleParams->theta0 = 107.0;
          } else {
            mmffAngleParams->theta0 = 92.0;
          }
        }
        break;
    }
    if (ringSize == 3) {
      mmffAngleParams->theta0 = 60.0;
    } else if (ringSize == 4) {
      mmffAngleParams->theta0 = 90.0;
    }
  } else {
    mmffAngleParams->theta0 = oldMMFFAngleParams->theta0;
  }
  // angle force constant empirical rule
  double Z[3] = {0.0, 0.0, 0.0};
  double C[3] = {0.0, 0.0, 0.0};
  double beta = 1.75;
  for (unsigned int i = 0; i < 3; ++i) {
    // Table VI - MMFF.V, page 628
    switch (atomicNum[i]) {
      // Hydrogen
      case 1:
        Z[i] = 1.395;
        break;

      // Carbon
      case 6:
        Z[i] = 2.494;
        C[i] = 1.016;
        break;

      // Nitrogen
      case 7:
        Z[i] = 2.711;
        C[i] = 1.113;
        break;

      // Oxygen
      case 8:
        Z[i] = 3.045;
        C[i] = 1.337;
        break;

      // Fluorine
      case 9:
        Z[i] = 2.847;
        break;

      // Silicon
      case 14:
        Z[i] = 2.350;
        C[i] = 0.811;
        break;

      // Phosphorus
      case 15:
        Z[i] = 2.350;
        C[i] = 1.068;
        break;

      // Sulfur
      case 16:
        Z[i] = 2.980;
        C[i] = 1.249;
        break;

      // Chlorine
      case 17:
        Z[i] = 2.909;
        C[i] = 1.078;
        break;

      // Bromine
      case 35:
        Z[i] = 3.017;
        break;

      // Iodine
      case 53:
        Z[i] = 3.086;
        break;
    }
  }
  double r0_ij = mmffBondParams1->r0;
  double r0_jk = mmffBondParams2->r0;
  double D =
      (r0_ij - r0_jk) * (r0_ij - r0_jk) / ((r0_ij + r0_jk) * (r0_ij + r0_jk));
  double theta0_rad = DEG2RAD * mmffAngleParams->theta0;
  if (ringSize == 4) {
    beta *= 0.85;
  } else if (ringSize == 3) {
    beta *= 0.05;
  }
  // equation (20) - MMFF.V, page 628
  mmffAngleParams->ka =
      beta * Z[0] * C[1] * Z[2] /
      ((r0_ij + r0_jk) * theta0_rad * theta0_rad * exp(2.0 * D));

  return (const ForceFields::MMFF::MMFFAngle *)mmffAngleParams;
}

// empirical rule to compute torsional parameters if
// tabulated parameters could not be found
// the indexes of the two central atoms J and K
// idx2 and idx3 must be supplied. The returned pointer
// to a MMFFTor object must be freed by the caller
const ForceFields::MMFF::MMFFTor *
MMFFMolProperties::getMMFFTorsionEmpiricalRuleParams(const ROMol &mol,
                                                     unsigned int idx2,
                                                     unsigned int idx3) const {
  PRECONDITION(this->isValid(), "missing atom types - invalid force-field");

  const MMFFPropCollection *mmffProp = DefaultParameters::getMMFFProp();
  const MMFFAromCollection *mmffArom = DefaultParameters::getMMFFArom();
  auto *mmffTorParams = new ForceFields::MMFF::MMFFTor();
  unsigned int jAtomType = this->getMMFFAtomType(idx2);
  unsigned int kAtomType = this->getMMFFAtomType(idx3);
  const MMFFProp *jMMFFProp = (*mmffProp)(jAtomType);
  const MMFFProp *kMMFFProp = (*mmffProp)(kAtomType);
  const Bond *bond = mol.getBondBetweenAtoms(idx2, idx3);
  double U[2] = {0.0, 0.0};
  double V[2] = {0.0, 0.0};
  double W[2] = {0.0, 0.0};
  double beta = 0.0;
  double pi_jk = 0.0;
  const auto N_jk = (double)((jMMFFProp->crd - 1) * (kMMFFProp->crd - 1));
  int atomicNum[2] = {mol.getAtomWithIdx(idx2)->getAtomicNum(),
                      mol.getAtomWithIdx(idx3)->getAtomicNum()};

  for (unsigned int i = 0; i < 2; ++i) {
    switch (atomicNum[i]) {
      // carbon
      case 6:
        U[i] = 2.0;
        V[i] = 2.12;
        break;

      // nitrogen
      case 7:
        U[i] = 2.0;
        V[i] = 1.5;
        break;

      // oxygen
      case 8:
        U[i] = 2.0;
        V[i] = 0.2;
        W[i] = 2.0;
        break;

      // silicon
      case 14:
        U[i] = 1.25;
        V[i] = 1.22;
        break;

      // phosphorus
      case 15:
        U[i] = 1.25;
        V[i] = 2.40;
        break;

      // sulfur
      case 16:
        U[i] = 1.25;
        V[i] = 0.49;
        W[i] = 8.0;
        break;
    }
  }

  // rule (a)
  if (jMMFFProp->linh || kMMFFProp->linh) {
    mmffTorParams->V1 = 0.0;
    mmffTorParams->V2 = 0.0;
    mmffTorParams->V3 = 0.0;
  }

  // rule (b)
  else if (mmffArom->isMMFFAromatic(jAtomType) &&
           mmffArom->isMMFFAromatic(kAtomType) && bond->getIsAromatic()) {
    beta = ((((jMMFFProp->val == 3) && (kMMFFProp->val == 4)) ||
             ((jMMFFProp->val == 4) && (kMMFFProp->val == 3)))
                ? 3.0
                : 6.0);
    pi_jk = (((jMMFFProp->pilp == 0) && (kMMFFProp->pilp == 0)) ? 0.5 : 0.3);
    mmffTorParams->V2 = beta * pi_jk * sqrt(U[0] * U[1]);
  }

  // rule (c)
  else if (bond->getBondType() == Bond::DOUBLE) {
    beta = 6.0;
    pi_jk = (((jMMFFProp->mltb == 2) && (kMMFFProp->mltb == 2)) ? 1.0 : 0.4);
    mmffTorParams->V2 = beta * pi_jk * sqrt(U[0] * U[1]);
  }

  // rule (d)
  else if ((jMMFFProp->crd == 4) && (kMMFFProp->crd == 4)) {
    mmffTorParams->V3 = sqrt(V[0] * V[1]) / N_jk;
  }

  // rule (e)
  else if ((jMMFFProp->crd == 4) && (kMMFFProp->crd != 4)) {
    if (((kMMFFProp->crd == 3) &&
         (((kMMFFProp->val == 4) || (kMMFFProp->val == 34)) ||
          kMMFFProp->mltb)) ||
        ((kMMFFProp->crd == 2) && ((kMMFFProp->val == 3) || kMMFFProp->mltb))) {
      mmffTorParams->V1 = 0.0;
      mmffTorParams->V2 = 0.0;
      mmffTorParams->V3 = 0.0;
    } else {
      mmffTorParams->V3 = sqrt(V[0] * V[1]) / N_jk;
    }
  }

  // rule (f)
  else if ((kMMFFProp->crd == 4) && (jMMFFProp->crd != 4)) {
    if (((jMMFFProp->crd == 3) &&
         (((jMMFFProp->val == 4) || (jMMFFProp->val == 34)) ||
          jMMFFProp->mltb)) ||
        ((jMMFFProp->crd == 2) && ((jMMFFProp->val == 3) || jMMFFProp->mltb))) {
      mmffTorParams->V1 = 0.0;
      mmffTorParams->V2 = 0.0;
      mmffTorParams->V3 = 0.0;
    } else {
      mmffTorParams->V3 = sqrt(V[0] * V[1]) / N_jk;
    }
  }

  // rule (g)
  else if (((bond->getBondType() == Bond::SINGLE) && jMMFFProp->mltb &&
            kMMFFProp->mltb) ||
           (jMMFFProp->mltb && kMMFFProp->pilp) ||
           (jMMFFProp->pilp && kMMFFProp->mltb)) {
    // case (1)
    if (jMMFFProp->pilp && kMMFFProp->pilp) {
      mmffTorParams->V1 = 0.0;
      mmffTorParams->V2 = 0.0;
      mmffTorParams->V3 = 0.0;
    }
    // case (2)
    else if (jMMFFProp->pilp && kMMFFProp->mltb) {
      beta = 6.0;
      if (jMMFFProp->mltb == 1) {
        pi_jk = 0.5;
      } else if ((getPeriodicTableRow(atomicNum[0]) == 2) &&
                 (getPeriodicTableRow(atomicNum[1]) == 2)) {
        pi_jk = 0.3;
      } else if ((getPeriodicTableRow(atomicNum[0]) != 2) ||
                 (getPeriodicTableRow(atomicNum[1]) != 2)) {
        pi_jk = 0.15;
      }
      mmffTorParams->V2 = beta * pi_jk * sqrt(U[0] * U[1]);
    }
    // case (3)
    else if (kMMFFProp->pilp && jMMFFProp->mltb) {
      beta = 6.0;
      if (kMMFFProp->mltb == 1) {
        pi_jk = 0.5;
      } else if ((getPeriodicTableRow(atomicNum[0]) == 2) &&
                 (getPeriodicTableRow(atomicNum[1]) == 2)) {
        pi_jk = 0.3;
      } else if ((getPeriodicTableRow(atomicNum[0]) != 2) ||
                 (getPeriodicTableRow(atomicNum[1]) != 2)) {
        pi_jk = 0.15;
      }
      mmffTorParams->V2 = beta * pi_jk * sqrt(U[0] * U[1]);
    }
    // case (4)
    else if (((jMMFFProp->mltb == 1) || (kMMFFProp->mltb == 1)) &&
             ((atomicNum[0] != 6) || (atomicNum[1] != 6))) {
      beta = 6.0;
      pi_jk = 0.4;
      mmffTorParams->V2 = beta * pi_jk * sqrt(U[0] * U[1]);
    }
    // case (5)
    else {
      beta = 6.0;
      pi_jk = 0.15;
      mmffTorParams->V2 = beta * pi_jk * sqrt(U[0] * U[1]);
    }
  }

  // rule (h)
  else {
    if (((atomicNum[0] == 8) || (atomicNum[0] == 16)) &&
        ((atomicNum[1] == 8) || (atomicNum[1] == 16))) {
      mmffTorParams->V2 = -sqrt(W[0] * W[1]);
    } else {
      mmffTorParams->V3 = sqrt(V[0] * V[1]) / N_jk;
    }
  }

  return (const MMFFTor *)mmffTorParams;
}

// populates the MMFFMolProperties object with MMFF
// formal and partial charges
void MMFFMolProperties::computeMMFFCharges(const ROMol &mol) {
  PRECONDITION(this->isValid(), "missing atom types - invalid force-field");

  unsigned int idx;
  unsigned int i;
  unsigned int j;
  unsigned int atomType;
  unsigned int nbrAtomType;
  unsigned int nConj = 0;
  unsigned int old_nConj = 0;
  double pChg = 0.0;
  double fChg = 0.0;
  boost::dynamic_bitset<> conjNBitVect(mol.getNumAtoms());
  VECT_INT_VECT atomRings = mol.getRingInfo()->atomRings();
  ROMol::ADJ_ITER nbrIdx;
  ROMol::ADJ_ITER endNbrs;
  ROMol::ADJ_ITER nbr2Idx;
  ROMol::ADJ_ITER end2Nbrs;
  const MMFFPropCollection *mmffProp = DefaultParameters::getMMFFProp();
  const MMFFPBCICollection *mmffPBCI = DefaultParameters::getMMFFPBCI();
  const MMFFChgCollection *mmffChg = DefaultParameters::getMMFFChg();

  // We need to set formal charges upfront
  for (idx = 0; idx < mol.getNumAtoms(); ++idx) {
    const Atom *atom = mol.getAtomWithIdx(idx);
    atomType = this->getMMFFAtomType(idx);
    fChg = 0.0;
    switch (atomType) {
      // special cases
      case 32:
      // O2CM
      // Oxygen in carboxylate group
      case 72:
        // SM
        // Anionic terminal sulfur
        // loop over neighbors
        boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
        for (; nbrIdx != endNbrs; ++nbrIdx) {
          const Atom *nbrAtom = mol[*nbrIdx];
          nbrAtomType = this->getMMFFAtomType(nbrAtom->getIdx());
          // loop over neighbors of the neighbor
          // count how many terminal oxygen/sulfur atoms
          // or secondary nitrogens
          // are bonded to the neighbor of ipso
          int nSecNbondedToNbr = 0;
          int nTermOSbondedToNbr = 0;
          boost::tie(nbr2Idx, end2Nbrs) = mol.getAtomNeighbors(nbrAtom);
          for (; nbr2Idx != end2Nbrs; ++nbr2Idx) {
            const Atom *nbr2Atom = mol[*nbr2Idx];
            // if it's nitrogen with 2 neighbors and it is not aromatic,
            // increment the counter of secondary nitrogens
            if ((nbr2Atom->getAtomicNum() == 7) &&
                (nbr2Atom->getDegree() == 2) &&
                (!(nbr2Atom->getIsAromatic()))) {
              ++nSecNbondedToNbr;
            }
            // if it's terminal oxygen/sulfur,
            // increment the terminal oxygen/sulfur counter
            if (((nbr2Atom->getAtomicNum() == 8) ||
                 (nbr2Atom->getAtomicNum() == 16)) &&
                (nbr2Atom->getDegree() == 1)) {
              ++nTermOSbondedToNbr;
            }
          }
          // in case its sulfur with two terminal oxygen/sulfur atoms and one
          // secondary
          // nitrogen, this is a deprotonated sulfonamide, so we should not
          // consider
          // nitrogen as a replacement for oxygen/sulfur in a sulfone
          if ((nbrAtom->getAtomicNum() == 16) && (nTermOSbondedToNbr == 2) &&
              (nSecNbondedToNbr == 1)) {
            nSecNbondedToNbr = 0;
          }
          // if the neighbor is carbon
          if ((nbrAtom->getAtomicNum() == 6) && nTermOSbondedToNbr) {
            // O2CM
            // Oxygen in (thio)carboxylate group: charge is shared
            // across 2 oxygens/sulfur atoms in (thio)carboxylate,
            // 3 oxygen/sulfur atoms in (thio)carbonate
            // SM
            // Anionic terminal sulfur: charge is localized
            fChg = ((nTermOSbondedToNbr == 1)
                        ? -1.0
                        : -((double)(nTermOSbondedToNbr - 1) /
                            (double)nTermOSbondedToNbr));
            break;
          }
          // if the neighbor is NO2 or NO3
          if ((nbrAtomType == 45) && (nTermOSbondedToNbr == 3)) {
            // O3N
            // Nitrate anion oxygen
            fChg = -1.0 / 3.0;
            break;
          }
          // if the neighbor is PO2, PO3, PO4
          if ((nbrAtomType == 25) && nTermOSbondedToNbr) {
            // OP
            // Oxygen in phosphine oxide
            // O2P
            // One of 2 terminal O's on P
            // O3P
            // One of 3 terminal O's on P
            // O4P
            // One of 4 terminal O's on P
            fChg = ((nTermOSbondedToNbr == 1)
                        ? 0.0
                        : -((double)(nTermOSbondedToNbr - 1) /
                            (double)nTermOSbondedToNbr));
            break;
          }
          // if the neighbor is SO2, SO2N, SO3, SO4, SO2M, SSOM
          if ((nbrAtomType == 18) && nTermOSbondedToNbr) {
            // SO2
            // Sulfone sulfur
            // SO2N
            // Sulfonamide sulfur
            // SO3
            // Sulfonate group sulfur
            // SO4
            // Sulfate group sulfur
            // SNO
            // Sulfur in nitrogen analog of a sulfone
            fChg =
                (((nSecNbondedToNbr + nTermOSbondedToNbr) == 2)
                     ? 0.0
                     : -((double)((nSecNbondedToNbr + nTermOSbondedToNbr) - 2) /
                         (double)nTermOSbondedToNbr));
            break;
          }
          if ((nbrAtomType == 73) && nTermOSbondedToNbr) {
            // SO2M
            // Sulfur in anionic sulfinate group
            // SSOM
            // Tricoordinate sulfur in anionic thiosulfinate group
            fChg = ((nTermOSbondedToNbr == 1)
                        ? 0.0
                        : -((double)(nTermOSbondedToNbr - 1) /
                            (double)nTermOSbondedToNbr));
            break;
          }
          if ((nbrAtomType == 77) && nTermOSbondedToNbr) {
            // O4Cl
            // Oxygen in perchlorate anion
            fChg = -(1.0 / (double)nTermOSbondedToNbr);
            break;
          }
        }
        break;

      case 76:
        // N5M
        // Nitrogen in 5-ring aromatic anion
        // we don't need to bother about the neighbors with N5M
        for (i = 0; i < atomRings.size(); ++i) {
          if ((std::find(atomRings[i].begin(), atomRings[i].end(), idx) !=
               atomRings[i].end())) {
            break;
          }
        }
        // find how many nitrogens with atom type 76 we have
        // and share the formal charge accordingly
        if (i < atomRings.size()) {
          unsigned int nNitrogensIn5Ring = 0;
          for (j = 0; j < atomRings[i].size(); ++j) {
            if (this->getMMFFAtomType(atomRings[i][j]) == 76) {
              ++nNitrogensIn5Ring;
            }
          }
          if (nNitrogensIn5Ring) {
            fChg = -(1.0 / (double)nNitrogensIn5Ring);
          }
        }
        break;

      case 55:
      case 56:
      case 81:
        // NIM+
        // Aromatic nitrogen in imidazolium
        // N5A+
        // Positive nitrogen in 5-ring alpha position
        // N5B+
        // Positive nitrogen in 5-ring beta position
        // N5+
        // Positive nitrogen in other 5-ring position
        // we need to loop over all molecule atoms
        // and find all those nitrogens with atom type
        // 81, 55 or 56, check whether they are conjugated
        // with ipso and keep on looping until no more
        // conjugated atoms can be found. Finally, we divide
        // the total formal charge that was found on the
        // conjugated system by the number of conjugated nitrogens
        // of types 81, 55 or 56 that were found.
        // This is not strictly what is described
        // in the MMFF papers, but it is the only way to get an
        // integer total formal charge, which makes sense to me
        // probably such conjugated systems are anyway out of the
        // scope of MMFF, but this is an attempt to correctly
        // deal with them somehow
        fChg = (double)(atom->getFormalCharge());
        nConj = 1;
        old_nConj = 0;
        conjNBitVect.reset();
        conjNBitVect[idx] = 1;
        while (nConj > old_nConj) {
          old_nConj = nConj;
          for (i = 0; i < mol.getNumAtoms(); ++i) {
            // if this atom is not marked as conj, move on
            if (!conjNBitVect[i]) {
              continue;
            }
            // loop over neighbors
            boost::tie(nbrIdx, endNbrs) =
                mol.getAtomNeighbors(mol.getAtomWithIdx(i));
            for (; nbrIdx != endNbrs; ++nbrIdx) {
              const Atom *nbrAtom = mol[*nbrIdx];
              nbrAtomType = this->getMMFFAtomType(nbrAtom->getIdx());
              // if atom type is not 80 or 57, move on
              if ((nbrAtomType != 57) && (nbrAtomType != 80)) {
                continue;
              }
              // loop over neighbors of the neighbor
              // if they are nitrogens of type 81, 55 or 56 and
              // they are not not marked as conjugated yet, do it
              // and increment the nConj counter by 1
              boost::tie(nbr2Idx, end2Nbrs) = mol.getAtomNeighbors(nbrAtom);
              for (; nbr2Idx != end2Nbrs; ++nbr2Idx) {
                const Atom *nbr2Atom = mol[*nbr2Idx];
                // if atom type is not 81, 55 or 56, move on
                nbrAtomType = this->getMMFFAtomType(nbr2Atom->getIdx());
                if ((nbrAtomType != 55) && (nbrAtomType != 56) &&
                    (nbrAtomType != 81)) {
                  continue;
                }
                j = nbr2Atom->getIdx();
                // if this nitrogen is not yet marked as conjugated,
                // mark it and increment the counter and eventually
                // adjust the total formal charge of the conjugated system
                if (!conjNBitVect[j]) {
                  conjNBitVect[j] = 1;
                  fChg += (double)(nbr2Atom->getFormalCharge());
                  ++nConj;
                }
              }
            }
          }
        }
        if (nConj) {
          fChg /= (double)nConj;
        }
        break;

      case 61:
        // loop over neighbors
        boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
        for (; nbrIdx != endNbrs; ++nbrIdx) {
          const Atom *nbrAtom = mol[*nbrIdx];
          // if it is diazonium, set a +1 formal charge on
          // the secondary nitrogen
          if (this->getMMFFAtomType(nbrAtom->getIdx()) == 42) {
            fChg = 1.0;
          }
        }
        break;

      // non-complicated +1 atom types
      case 34:
      // NR+
      // Quaternary nitrogen
      case 49:
      // O+
      // Oxonium oxygen
      case 51:
      // O=+
      // Oxenium oxygen
      case 54:
      // N+=C
      // Iminium nitrogen
      // N+=N
      // Positively charged nitrogen doubly bonded to N
      case 58:
      // NPD+
      // Aromatic nitrogen in pyridinium
      case 92:
      // LI+
      // Lithium cation
      case 93:
      // NA+
      // Sodium cation
      case 94:
      // K+
      // Potassium cation
      case 97:
        // CU+1
        // Monopositive copper cation
        fChg = 1.0;
        break;

      // non-complicated +2 atom types
      case 87:
      // FE+2
      // Dipositive iron cation
      case 95:
      // ZN+2
      // Dipositive zinc cation
      case 96:
      // CA+2
      // Dipositive calcium cation
      case 98:
      // CU+2
      // Dipositive copper cation
      case 99:
        // MG+2
        // Dipositive magnesium cation
        fChg = 2.0;
        break;

      // non-complicated +3 atom types
      case 88:
        // FE+3
        // Tripositive iron cation
        fChg = 3.0;
        break;

      // non-complicated -1 atom types
      case 35:
      // OM
      // Oxide oxygen on sp3 carbon
      // OM2
      // Oxide oxygen on sp2 carbon
      // OM
      // Oxide oxygen on sp3 nitrogen (not in original MMFF.I Table III)
      // OM2
      // Oxide oxygen on sp2 nitrogen (not in original MMFF.I Table III)
      case 62:
      // NM
      // Anionic divalent nitrogen
      case 89:
      // F-
      // Fluoride anion
      case 90:
      // Cl-
      // Chloride anion
      case 91:
        // BR-
        // Bromide anion
        fChg = -1.0;
        break;
    }
    this->setMMFFFormalCharge(idx, fChg);
  }
  // now we compute partial charges
  // See Halgren, T. MMFF.V, J. Comput. Chem. 1996, 17, 616-641
  // https://doi.org/10.1002/(SICI)1096-987X(199604)17:5/6<616::AID-JCC5>3.0.CO;2-X
  for (idx = 0; idx < mol.getNumAtoms(); ++idx) {
    const Atom *atom = mol.getAtomWithIdx(idx);
    atomType = this->getMMFFAtomType(idx);
    double q0 = this->getMMFFFormalCharge(idx);
    auto M = (double)((*mmffProp)(atomType)->crd);
    double v = (*mmffPBCI)(atomType)->fcadj;
    double sumFormalCharge = 0.0;
    double sumPartialCharge = 0.0;
    double nbrFormalCharge;
    std::pair<int, const MMFFChg *> mmffChgParams;

    if (isDoubleZero(v)) {
      // loop over neighbors
      boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
      for (; nbrIdx != endNbrs; ++nbrIdx) {
        const Atom *nbrAtom = mol[*nbrIdx];
        nbrFormalCharge = this->getMMFFFormalCharge(nbrAtom->getIdx());
        // if neighbors have a negative formal charge, the latter
        // influences the charge on ipso
        if (nbrFormalCharge < 0.0) {
          q0 += (nbrFormalCharge / (2.0 * (double)(nbrAtom->getDegree())));
        }
      }
    }
    // there is a special case for anionic divalent nitrogen
    // with positively charged neighbor
    if (atomType == 62) {
      boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
      for (; nbrIdx != endNbrs; ++nbrIdx) {
        const Atom *nbrAtom = mol[*nbrIdx];
        nbrFormalCharge = this->getMMFFFormalCharge(nbrAtom->getIdx());
        if (nbrFormalCharge > 0.0) {
          q0 -= (nbrFormalCharge / 2.0);
        }
      }
    }
    // loop over neighbors
    boost::tie(nbrIdx, endNbrs) = mol.getAtomNeighbors(atom);
    for (; nbrIdx != endNbrs; ++nbrIdx) {
      const Atom *nbrAtom = mol[*nbrIdx];
      const Bond *bond =
          mol.getBondBetweenAtoms(atom->getIdx(), nbrAtom->getIdx());
      // we need to determine the sign of bond charge
      // increments depending on the bonding relationship
      // i.e. we have parameters for [a,b] bonds
      // but it depends whether ipso is a or b
      unsigned int nbrAtomType = this->getMMFFAtomType(nbrAtom->getIdx());
      unsigned int bondType = this->getMMFFBondType(bond);
      mmffChgParams =
          mmffChg->getMMFFChgParams(bondType, atomType, nbrAtomType);
      sumPartialCharge +=
          (mmffChgParams.second
               ? (double)(mmffChgParams.first) * ((mmffChgParams.second)->bci)
               : ((*mmffPBCI)(atomType)->pbci -
                  (*mmffPBCI)(nbrAtomType)->pbci));
      nbrFormalCharge = this->getMMFFFormalCharge(nbrAtom->getIdx());
      sumFormalCharge += nbrFormalCharge;
    }
    // we compute ipso partial charge according to
    // equation 15, page 622 MMFF.V paper
    pChg = (1.0 - M * v) * q0 + v * sumFormalCharge + sumPartialCharge;
    this->setMMFFPartialCharge(atom->getIdx(), pChg);
  }
}

bool MMFFMolProperties::getMMFFBondStretchParams(
    const ROMol &mol, const unsigned int idx1, const unsigned int idx2,
    unsigned int &bondType, MMFFBond &mmffBondStretchParams) const {
  const MMFFBondCollection *mmffBond = DefaultParameters::getMMFFBond();
  bool res = false;
  if (isValid()) {
    unsigned int iAtomType = getMMFFAtomType(idx1);
    unsigned int jAtomType = getMMFFAtomType(idx2);
    const Bond *bond = mol.getBondBetweenAtoms(idx1, idx2);
    if (bond) {
      bondType = getMMFFBondType(bond);
      bool areMMFFBondParamsEmpirical = false;
      const MMFFBond *mmffBondParams =
          (*mmffBond)(bondType, iAtomType, jAtomType);
      if (!mmffBondParams) {
        mmffBondParams = getMMFFBondStretchEmpiricalRuleParams(mol, bond);
        areMMFFBondParamsEmpirical = true;
      }
      if (mmffBondParams) {
        mmffBondStretchParams = *mmffBondParams;
        if (areMMFFBondParamsEmpirical) {
          delete mmffBondParams;
        }
        res = true;
      }
    }
  }
  return res;
}

bool MMFFMolProperties::getMMFFAngleBendParams(
    const ROMol &mol, const unsigned int idx1, const unsigned int idx2,
    const unsigned int idx3, unsigned int &angleType,
    MMFFAngle &mmffAngleBendParams) const {
  bool res = false;
  if (isValid() && mol.getBondBetweenAtoms(idx1, idx2) &&
      mol.getBondBetweenAtoms(idx2, idx3)) {
    const MMFFAngleCollection *mmffAngle = DefaultParameters::getMMFFAngle();
    const MMFFPropCollection *mmffProp = DefaultParameters::getMMFFProp();
    unsigned int idx[3] = {idx1, idx2, idx3};
    MMFFBond mmffBondParams[2];
    unsigned int atomType[3];
    unsigned int i;
    angleType = getMMFFAngleType(mol, idx1, idx2, idx3);
    bool areMMFFAngleParamsEmpirical = false;
    for (i = 0; i < 3; ++i) {
      atomType[i] = getMMFFAtomType(idx[i]);
    }
    const MMFFAngle *mmffAngleParams =
        (*mmffAngle)(DefaultParameters::getMMFFDef(), angleType, atomType[0],
                     atomType[1], atomType[2]);
    const MMFFProp *mmffPropParamsCentralAtom = (*mmffProp)(atomType[1]);
    if ((!mmffAngleParams) || (isDoubleZero(mmffAngleParams->ka))) {
      areMMFFAngleParamsEmpirical = true;
      for (i = 0; areMMFFAngleParamsEmpirical && (i < 2); ++i) {
        unsigned int bondType;
        areMMFFAngleParamsEmpirical = getMMFFBondStretchParams(
            mol, idx[i], idx[i + 1], bondType, mmffBondParams[i]);
      }
      if (areMMFFAngleParamsEmpirical) {
        mmffAngleParams = getMMFFAngleBendEmpiricalRuleParams(
            mol, mmffAngleParams, mmffPropParamsCentralAtom, &mmffBondParams[0],
            &mmffBondParams[1], idx[0], idx[1], idx[2]);
      }
    }
    if (mmffAngleParams) {
      mmffAngleBendParams = *mmffAngleParams;
      res = true;
      if (areMMFFAngleParamsEmpirical) {
        delete mmffAngleParams;
      }
    }
  }
  return res;
}

bool MMFFMolProperties::getMMFFStretchBendParams(
    const ROMol &mol, const unsigned int idx1, const unsigned int idx2,
    const unsigned int idx3, unsigned int &stretchBendType,
    MMFFStbn &mmffStretchBendParams, MMFFBond mmffBondStretchParams[2],
    MMFFAngle &mmffAngleBendParams) const {
  bool res = false;
  if (isValid()) {
    const MMFFPropCollection *mmffProp = DefaultParameters::getMMFFProp();
    const MMFFStbnCollection *mmffStbn = DefaultParameters::getMMFFStbn();
    const MMFFDfsbCollection *mmffDfsb = DefaultParameters::getMMFFDfsb();
    unsigned int idx[3] = {idx1, idx2, idx3};
    unsigned int atomType[3];
    unsigned int bondType[2];
    unsigned int angleType;
    const MMFFProp *mmffPropParamsCentralAtom =
        (*mmffProp)(getMMFFAtomType(idx[1]));
    if (!(mmffPropParamsCentralAtom->linh)) {
      res = true;
      unsigned int i = 0;
      for (i = 0; i < 3; ++i) {
        atomType[i] = getMMFFAtomType(idx[i]);
      }
      for (i = 0; res && (i < 2); ++i) {
        res = getMMFFBondStretchParams(mol, idx[i], idx[i + 1], bondType[i],
                                       mmffBondStretchParams[i]);
      }
      if (res) {
        res = getMMFFAngleBendParams(mol, idx1, idx2, idx3, angleType,
                                     mmffAngleBendParams);
      }
      std::pair<bool, const MMFFStbn *> mmffStbnParams;
      if (res) {
        stretchBendType = getMMFFStretchBendType(
            angleType, (atomType[0] <= atomType[2]) ? bondType[0] : bondType[1],
            (atomType[0] < atomType[2]) ? bondType[1] : bondType[0]);
        mmffStbnParams = mmffStbn->getMMFFStbnParams(
            stretchBendType, bondType[0], bondType[1], atomType[0], atomType[1],
            atomType[2]);
        if (!(mmffStbnParams.second)) {
          mmffStbnParams = mmffDfsb->getMMFFDfsbParams(
              getPeriodicTableRow(mol.getAtomWithIdx(idx1)->getAtomicNum()),
              getPeriodicTableRow(mol.getAtomWithIdx(idx2)->getAtomicNum()),
              getPeriodicTableRow(mol.getAtomWithIdx(idx3)->getAtomicNum()));
        }
        res = (mmffStbnParams.second &&
               !(isDoubleZero((mmffStbnParams.second)->kbaIJK) &&
                 isDoubleZero((mmffStbnParams.second)->kbaKJI)));
      }
      if (res) {
        if (mmffStbnParams.first) {
          mmffStretchBendParams.kbaIJK = (mmffStbnParams.second)->kbaKJI;
          mmffStretchBendParams.kbaKJI = (mmffStbnParams.second)->kbaIJK;
        } else {
          mmffStretchBendParams = *(mmffStbnParams.second);
        }
      }
    }
  }
  return res;
}

bool MMFFMolProperties::getMMFFTorsionParams(
    const ROMol &mol, const unsigned int idx1, const unsigned int idx2,
    const unsigned int idx3, const unsigned int idx4, unsigned int &torsionType,
    MMFFTor &mmffTorsionParams) const {
  bool res = false;
  if (isValid() && mol.getBondBetweenAtoms(idx1, idx2) &&
      mol.getBondBetweenAtoms(idx2, idx3) &&
      mol.getBondBetweenAtoms(idx3, idx4)) {
    unsigned int i;
    unsigned int idx[4] = {idx1, idx2, idx3, idx4};
    unsigned int atomType[4];
    const MMFFTorCollection *mmffTor =
        DefaultParameters::getMMFFTor(getMMFFVariant() == "MMFF94s");
    for (i = 0; i < 4; ++i) {
      atomType[i] = getMMFFAtomType(idx[i]);
    }
    const std::pair<unsigned int, unsigned int> torTypePair =
        getMMFFTorsionType(mol, idx1, idx2, idx3, idx4);
    bool areMMFFTorParamsEmpirical = false;
    const std::pair<const unsigned int, const MMFFTor *> mmffTorPair =
        mmffTor->getMMFFTorParams(DefaultParameters::getMMFFDef(), torTypePair,
                                  atomType[0], atomType[1], atomType[2],
                                  atomType[3]);
    torsionType = (mmffTorPair.first ? mmffTorPair.first : torTypePair.first);
    const MMFFTor *mmffTorParams = mmffTorPair.second;
    if (!mmffTorParams) {
      torsionType = torTypePair.first;
      mmffTorParams = getMMFFTorsionEmpiricalRuleParams(mol, idx2, idx3);
      areMMFFTorParamsEmpirical = true;
    }
    res =
        (!(isDoubleZero(mmffTorParams->V1) && isDoubleZero(mmffTorParams->V2) &&
           isDoubleZero(mmffTorParams->V3)));
    if (res) {
      mmffTorsionParams = *mmffTorParams;
    }
    if (areMMFFTorParamsEmpirical) {
      delete mmffTorParams;
    }
  }
  return res;
}

bool MMFFMolProperties::getMMFFOopBendParams(
    const ROMol &mol, const unsigned int idx1, const unsigned int idx2,
    const unsigned int idx3, const unsigned int idx4,
    MMFFOop &mmffOopBendParams) const {
  bool res = false;
  if (isValid() && mol.getBondBetweenAtoms(idx1, idx2) &&
      mol.getBondBetweenAtoms(idx2, idx3) &&
      mol.getBondBetweenAtoms(idx2, idx4)) {
    unsigned int i;
    unsigned int idx[4] = {idx1, idx2, idx3, idx4};
    unsigned int atomType[4];

    const MMFFOopCollection *mmffOop =
        DefaultParameters::getMMFFOop(getMMFFVariant() == "MMFF94s");
    for (i = 0; i < 4; ++i) {
      atomType[i] = getMMFFAtomType(idx[i]);
    }
    const MMFFOop *mmffOopParams =
        (*mmffOop)(DefaultParameters::getMMFFDef(), atomType[0], atomType[1],
                   atomType[2], atomType[3]);
    // if no parameters could be found, we exclude this term (SURDOX02)
    if (mmffOopParams) {
      mmffOopBendParams = *mmffOopParams;
      res = true;
    }
  }
  return res;
}

bool MMFFMolProperties::getMMFFVdWParams(const unsigned int idx1,
                                         const unsigned int idx2,
                                         MMFFVdWRijstarEps &mmffVdWParams) const {
  bool res = false;
  if (isValid()) {
    const MMFFVdWCollection *mmffVdW = DefaultParameters::getMMFFVdW();
    const unsigned int iAtomType = getMMFFAtomType(idx1);
    const unsigned int jAtomType = getMMFFAtomType(idx2);
    const MMFFVdW *mmffVdWParamsIAtom = (*mmffVdW)(iAtomType);
    const MMFFVdW *mmffVdWParamsJAtom = (*mmffVdW)(jAtomType);
    if (mmffVdWParamsIAtom && mmffVdWParamsJAtom) {
      mmffVdWParams.R_ij_starUnscaled = MMFF::Utils::calcUnscaledVdWMinimum(
          mmffVdW, mmffVdWParamsIAtom, mmffVdWParamsJAtom);
      mmffVdWParams.epsilonUnscaled = MMFF::Utils::calcUnscaledVdWWellDepth(
          mmffVdWParams.R_ij_starUnscaled, mmffVdWParamsIAtom,
          mmffVdWParamsJAtom);
      mmffVdWParams.R_ij_star = mmffVdWParams.R_ij_starUnscaled;
      mmffVdWParams.epsilon = mmffVdWParams.epsilonUnscaled;
      MMFF::Utils::scaleVdWParams(mmffVdWParams.R_ij_star,
                                  mmffVdWParams.epsilon, mmffVdW,
                                  mmffVdWParamsIAtom, mmffVdWParamsJAtom);
      res = true;
    }
  }
  return res;
}
}  // namespace MMFF
}  // namespace RDKit
