// $Id$
//
// Copyright (C)  2013 Paolo Tosco
//
//   @@ 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 <RDGeneral/test.h>
#include <GraphMol/RDKitBase.h>
#include <GraphMol/FileParsers/MolSupplier.h>
#include <GraphMol/FileParsers/MolWriters.h>
#include <GraphMol/FileParsers/FileParsers.h>
#include <ForceField/MMFF/Params.h>
#include <ForceField/MMFF/DistanceConstraint.h>
#include <ForceField/MMFF/AngleConstraint.h>
#include <ForceField/MMFF/TorsionConstraint.h>
#include <ForceField/MMFF/PositionConstraint.h>
#include <GraphMol/ForceFieldHelpers/MMFF/AtomTyper.h>
#include <GraphMol/ForceFieldHelpers/MMFF/Builder.h>
#include <GraphMol/MolOps.h>
#include <GraphMol/DistGeomHelpers/Embedder.h>
#include <GraphMol/MolTransforms/MolTransforms.h>
#include <ForceField/ForceField.h>
#include <iostream>
#include <iomanip>
#include <fstream>
#include <sstream>
#include <string>
#include "testMMFFForceField.h"

#define FCON_TOLERANCE 0.05
#define ENERGY_TOLERANCE 0.025

using namespace RDKit;

bool fexist(std::string filename) {
  std::ifstream ifStream(filename.c_str());

  return (ifStream ? true : false);
}

bool fgrep(std::fstream &fStream, std::string key, std::string &line) {
  bool found = false;
  std::streampos current = fStream.tellg();
  while ((!found) &&
         (!(std::getline(fStream, line).rdstate() & std::ifstream::failbit))) {
    found = (line.find(key) != std::string::npos);
  }
  if (!found) {
    fStream.seekg(current);
  }

  return found;
}

bool fgrep(std::fstream &rdkFStream, std::string key) {
  std::string line;

  return fgrep(rdkFStream, key, line);
}

bool getLineByNum(std::fstream &stream, std::streampos startPos, unsigned int n,
                  std::string &line) {
  std::streampos current = stream.tellg();
  stream.seekg(startPos);
  bool fail;
  unsigned int i = 0;
  while ((i <= n) && (!(fail = std::getline(stream, line).rdstate() &
                               std::ifstream::failbit))) {
    ++i;
  }
  stream.seekg(current);

  return (!fail);
}

bool sortBondStretchInstanceVec(BondStretchInstance *a,
                                BondStretchInstance *b) {
  bool crit = false;

  if (a->iAtomType == b->iAtomType) {
    if (a->jAtomType == b->jAtomType) {
      crit = (a->ffType < b->ffType);
    } else {
      crit = (a->jAtomType < b->jAtomType);
    }
  } else {
    crit = (a->iAtomType < b->iAtomType);
  }

  return crit;
}

bool sortAngleBendInstanceVec(AngleBendInstance *a, AngleBendInstance *b) {
  bool crit = false;

  if (a->jAtomType == b->jAtomType) {
    if (a->iAtomType == b->iAtomType) {
      if (a->kAtomType == b->kAtomType) {
        crit = (a->ffType < b->ffType);
      } else {
        crit = (a->kAtomType < b->kAtomType);
      }
    } else {
      crit = (a->iAtomType < b->iAtomType);
    }
  } else {
    crit = (a->jAtomType < b->jAtomType);
  }

  return crit;
}

bool sortStretchBendInstanceVec(StretchBendInstance *a,
                                StretchBendInstance *b) {
  bool crit = false;

  if (a->jAtomType == b->jAtomType) {
    if (a->iAtomType == b->iAtomType) {
      if (a->kAtomType == b->kAtomType) {
        crit = (a->ffType < b->ffType);
      } else {
        crit = (a->kAtomType < b->kAtomType);
      }
    } else {
      crit = (a->iAtomType < b->iAtomType);
    }
  } else {
    crit = (a->jAtomType < b->jAtomType);
  }

  return crit;
}

bool sortOopBendInstanceVec(OopBendInstance *a, OopBendInstance *b) {
  bool crit = false;

  if (a->jAtomType == b->jAtomType) {
    if (a->iAtomType == b->iAtomType) {
      if (a->kAtomType == b->kAtomType) {
        crit = (a->lAtomType < b->lAtomType);
      } else {
        crit = (a->kAtomType < b->kAtomType);
      }
    } else {
      crit = (a->iAtomType < b->iAtomType);
    }
  } else {
    crit = (a->jAtomType < b->jAtomType);
  }

  return crit;
}

bool sortTorsionInstanceVec(TorsionInstance *a, TorsionInstance *b) {
  bool crit = false;

  if (a->jAtomType == b->jAtomType) {
    if (a->kAtomType == b->kAtomType) {
      if (a->iAtomType == b->iAtomType) {
        if (a->lAtomType == b->lAtomType) {
          crit = (a->ffType < b->ffType);
        } else {
          crit = (a->lAtomType < b->lAtomType);
        }
      } else {
        crit = (a->iAtomType < b->iAtomType);
      }
    } else {
      crit = (a->kAtomType < b->kAtomType);
    }
  } else {
    crit = (a->jAtomType < b->jAtomType);
  }

  return crit;
}

void fixBondStretchInstance(BondStretchInstance *bondStretchInstance) {
  if (bondStretchInstance->iAtomType > bondStretchInstance->jAtomType) {
    unsigned int temp;
    temp = bondStretchInstance->iAtomType;
    bondStretchInstance->iAtomType = bondStretchInstance->jAtomType;
    bondStretchInstance->jAtomType = temp;
  }
}

void fixAngleBendInstance(AngleBendInstance *angleBendInstance) {
  if (angleBendInstance->iAtomType > angleBendInstance->kAtomType) {
    unsigned int temp;
    temp = angleBendInstance->iAtomType;
    angleBendInstance->iAtomType = angleBendInstance->kAtomType;
    angleBendInstance->kAtomType = temp;
  }
}

void fixOopBendInstance(OopBendInstance *oopBendInstance) {
  if (oopBendInstance->iAtomType > oopBendInstance->kAtomType) {
    unsigned int temp;
    temp = oopBendInstance->iAtomType;
    oopBendInstance->iAtomType = oopBendInstance->kAtomType;
    oopBendInstance->kAtomType = temp;
  }
}

void fixTorsionInstance(TorsionInstance *torsionInstance) {
  if (torsionInstance->jAtomType > torsionInstance->kAtomType) {
    unsigned int temp;
    temp = torsionInstance->jAtomType;
    torsionInstance->jAtomType = torsionInstance->kAtomType;
    torsionInstance->kAtomType = temp;
    temp = torsionInstance->iAtomType;
    torsionInstance->iAtomType = torsionInstance->lAtomType;
    torsionInstance->lAtomType = temp;
  } else if (torsionInstance->jAtomType == torsionInstance->kAtomType) {
    unsigned int temp;
    if (torsionInstance->iAtomType > torsionInstance->lAtomType) {
      temp = torsionInstance->iAtomType;
      torsionInstance->iAtomType = torsionInstance->lAtomType;
      torsionInstance->lAtomType = temp;
    }
  }
}

int mmffValidationSuite(int argc, char *argv[]) {
  std::string arg;
  std::string ffVariant = "";
  std::vector<std::string> ffVec;
  std::string verbosity;
  std::string molFile = "";
  std::string molType = "";
  std::vector<std::string> molFileVec;
  std::vector<std::string> molTypeVec;
  std::string rdk = "";
  std::string ref = "";
  std::streampos rdkPos;
  std::streampos rdkCurrent;
  std::streampos refPos;
  std::streampos refCurrent;
  int iarg = 1;
  unsigned int n = 0;
  bool error = false;
  bool fullTest = false;
  int inc = 4;
  bool testFailure = false;
  while (iarg < argc) {
    arg = argv[iarg];
    error = (arg.at(0) != '-');
    if (error) {
      break;
    }
    error = ((arg == "-h") || (arg == "--help"));
    if (error) {
      break;
    } else if (arg == "-f") {
      error = ((iarg + 1) >= argc);
      if (error) {
        break;
      }
      ffVariant = argv[iarg + 1];
      error = ((ffVariant != "MMFF94") && (ffVariant != "MMFF94s"));
      if (error) {
        break;
      }
      ++iarg;
    } else if ((arg == "-sdf") || (arg == "-smi")) {
      molType = arg.substr(1);
      if ((iarg + 1) < argc) {
        molFile = argv[iarg + 1];
        ++iarg;
      }
    } else if (arg == "-L") {
      fullTest = true;
      inc = 1;
    } else if (arg == "-l") {
      error = ((iarg + 1) >= argc);
      if (error) {
        break;
      }
      rdk = argv[iarg + 1];
      ++iarg;
    }
    ++iarg;
  }
  if (error) {
    std::cerr << "Usage: testMMFFForceField [-L] [-f {MMFF94|MMFF94s}] "
                 "[{-sdf [<sdf_file>] | -smi [<smiles_file>]}] [-l <log_file>]"
              << std::endl;

    return -1;
  }
  std::cerr << "-------------------------------------" << std::endl;
  std::cerr << "Official MMFF validation suite." << std::endl;
  std::string pathName = getenv("RDBASE");
  pathName += "/Code/ForceField/MMFF/test_data/";
  if (molFile != "") {
    ffVariant = ((molFile.substr(0, 7) == "MMFF94s") ? "MMFF94s" : "MMFF94");
  }
  if (ffVariant == "") {
    ffVec.push_back("MMFF94");
    if (fullTest) {
      ffVec.push_back("MMFF94s");
    }
  } else {
    ffVec.push_back(ffVariant);
  }
  std::fstream rdkFStream;
  std::fstream refFStream;
  if (rdk == "") {
    rdk = pathName + "testMMFFForceField.log";
  }
  bool firstTest = true;
  if (molType == "") {
    molTypeVec.push_back("sdf");
    molTypeVec.push_back("smi");
  } else {
    molTypeVec.push_back(molType);
  }
  boost::logging::disable_logs("rdApp.error");
  for (auto &molTypeIt : molTypeVec) {
    bool checkEnergy = (molTypeIt == "sdf");
    for (auto &ffIt : ffVec) {
      ref = pathName + ffIt + "_reference.log";
      molFileVec.clear();
      if (molFile == "") {
        molFileVec.push_back(pathName + ffIt + "_dative." + molTypeIt);
        molFileVec.push_back(pathName + ffIt + "_hypervalent." + molTypeIt);
      } else {
        molFileVec.push_back(molFile);
      }
      for (auto &molFileIt : molFileVec) {
        std::vector<ROMol *> molVec;
        molVec.clear();
        if (molTypeIt == "sdf") {
          SDMolSupplier sdfSupplier(molFileIt, false, false);
          for (size_t ii = 0; ii < sdfSupplier.length(); ++ii) {
            molVec.push_back(sdfSupplier[ii]);
          }
          sdfSupplier.reset();
        } else {
          SmilesMolSupplier smiSupplier(molFileIt);
          for (size_t ii = 0; ii < smiSupplier.length(); ++ii) {
            molVec.push_back(smiSupplier[ii]);
          }
          smiSupplier.reset();
        }
        SDWriter *sdfWriter =
            new SDWriter(molFileIt.substr(0, molFileIt.length() - 4) + "_min" +
                         ((molTypeIt == "smi") ? "_from_SMILES" : "") + ".sdf");
        ROMol *mol;
        std::string molName;
        std::vector<std::string> nameArray;

        rdkFStream.open(rdk.c_str(),
                        (firstTest ? std::fstream::out | std::fstream::binary
                                   : (std::fstream::out | std::fstream::binary |
                                      std::fstream::app)));
        std::string computingKey = ffIt + " energies for " + molFileIt;
        std::cerr << std::endl
                  << "Computing " << computingKey << "..." << std::endl;
        for (size_t ii = 0; ii < computingKey.length(); ++ii) {
          rdkFStream << "*";
        }
        rdkFStream << std::endl << computingKey << std::endl;
        for (size_t ii = 0; ii < computingKey.length(); ++ii) {
          rdkFStream << "*";
        }
        rdkFStream << std::endl << std::endl;
        for (size_t ii = 0; ii < molVec.size(); ii += inc) {
          mol = molVec[ii];
          if (molTypeIt == "smi") {
            DGeomHelpers::EmbedMolecule(*mol);
            MolOps::addHs((RWMol &)*mol, false, true);
          }
          MMFF::sanitizeMMFFMol((RWMol &)(*mol));
          if (mol->hasProp(common_properties::_Name)) {
            mol->getProp(common_properties::_Name, molName);
            rdkFStream << molName << std::endl;
            nameArray.push_back(molName);
          }
          auto *mmffMolProperties = new MMFF::MMFFMolProperties(
              *mol, ffIt, MMFF::MMFF_VERBOSITY_HIGH, rdkFStream);
          if ((!mmffMolProperties) || (!(mmffMolProperties->isValid()))) {
            std::cerr << molName + ": error setting up force-field"
                      << std::endl;
            continue;
          } else {
            ForceFields::ForceField *field = MMFF::constructForceField(
                *mol, mmffMolProperties, 100.0, -1, false);
            field->initialize();
            field->minimize();
            sdfWriter->write(*mol);
            rdkFStream << "\nTOTAL MMFF ENERGY              =" << std::right
                       << std::setw(16) << std::fixed << std::setprecision(4)
                       << field->calcEnergy() << "\n\n"
                       << std::endl;
            delete field;
          }
          delete mmffMolProperties;
        }
        BOOST_FOREACH (ROMol *m, molVec) { delete m; }
        sdfWriter->close();
        delete sdfWriter;
        rdkFStream.close();
        std::cerr << "Checking against " << ref << "..." << std::endl;
        rdkFStream.open(rdk.c_str(), std::fstream::in | std::fstream::binary);
        refFStream.open(ref.c_str(), std::fstream::in | std::fstream::binary);

        bool found = false;
        std::string skip;
        std::string errorMsg;
        std::string corruptedMsg;
        std::string energyString;
        std::vector<BondStretchInstance *> rdkBondStretchInstanceVec;
        std::vector<BondStretchInstance *> refBondStretchInstanceVec;
        std::vector<AngleBendInstance *> rdkAngleBendInstanceVec;
        std::vector<AngleBendInstance *> refAngleBendInstanceVec;
        std::vector<StretchBendInstance *> rdkStretchBendInstanceVec;
        std::vector<StretchBendInstance *> refStretchBendInstanceVec;
        std::vector<OopBendInstance *> rdkOopBendInstanceVec;
        std::vector<OopBendInstance *> refOopBendInstanceVec;
        std::vector<TorsionInstance *> rdkTorsionInstanceVec;
        std::vector<TorsionInstance *> refTorsionInstanceVec;
        double rdkEnergy;
        double rdkEleEnergy;
        double refEnergy;
        double refVdWEnergy;
        double refEleEnergy;
        unsigned int n_failed = 0;
        bool failed = false;
        rdkFStream.seekg(0);
        fgrep(rdkFStream, computingKey);
        size_t ii;
        for (ii = 0; ii < nameArray.size(); ii += inc) {
          error = false;
          failed = false;
          errorMsg = rdk + ": Molecule not found";
          found = fgrep(rdkFStream, nameArray[ii]);
          if (!found) {
            break;
          }
          errorMsg = ref + ": Molecule not found";
          found = fgrep(refFStream, nameArray[ii]);
          if (!found) {
            break;
          }
          refPos = refFStream.tellg();
          found = false;
          bool refHaveBondStretch = false;
          bool refHaveAngleBend = false;
          bool refHaveStretchBend = false;
          bool refHaveOopBend = false;
          bool refHaveTorsion = false;
          bool refHaveVdW = false;
          bool refHaveEle = false;
          std::string rdkLine;
          std::string refLine;
          while ((!found) && (!(std::getline(refFStream, refLine).rdstate() &
                                std::ifstream::failbit))) {
            std::size_t occ = refLine.find("****");
            found = (occ != std::string::npos);
            if (!found) {
              if (!refHaveVdW) {
                occ = refLine.find("Net vdW");
                refHaveVdW = (occ != std::string::npos);
                if (refHaveVdW) {
                  std::stringstream refVdWStringStream(refLine);
                  refVdWStringStream >> skip >> skip >> refVdWEnergy;
                }
              }
              if (!refHaveEle) {
                occ = refLine.find("Electrostatic");
                refHaveEle = (occ != std::string::npos);
                if (refHaveEle) {
                  std::stringstream refEleStringStream(refLine);
                  refEleStringStream >> skip >> refEleEnergy;
                }
              }
              if (!refHaveBondStretch) {
                occ = refLine.find("B O N D   S T R E T C H I N G");
                refHaveBondStretch = (occ != std::string::npos);
              }
              if (!refHaveAngleBend) {
                occ = refLine.find("A N G L E   B E N D I N G");
                refHaveAngleBend = (occ != std::string::npos);
              }
              if (!refHaveStretchBend) {
                occ = refLine.find("S T R E T C H   B E N D I N G");
                refHaveStretchBend = (occ != std::string::npos);
              }
              if (!refHaveOopBend) {
                occ = refLine.find("O U T - O F - P L A N E    B E N D I N G");
                refHaveOopBend = (occ != std::string::npos);
              }
              if (!refHaveTorsion) {
                occ = refLine.find("T O R S I O N A L");
                refHaveTorsion = (occ != std::string::npos);
              }
            }
          }
          refFStream.seekg(refPos);
          errorMsg = "";

          // --------------------------------------------------
          // B O N D   S T R E T C H I N G
          // --------------------------------------------------
          if (refHaveBondStretch) {
            corruptedMsg = ": Bond stretching: corrupted input data\n";
            found = fgrep(rdkFStream, "B O N D   S T R E T C H I N G");
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            found = fgrep(rdkFStream, "----------------");
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            found = false;
            rdkBondStretchInstanceVec.clear();
            rdkPos = rdkFStream.tellg();
            n = 0;
            while ((!found) && (!(std::getline(rdkFStream, rdkLine).rdstate() &
                                  std::ifstream::failbit))) {
              found = (!(rdkLine.length()));
              if (found) {
                break;
              }
              std::stringstream rdkLineStream(rdkLine);
              auto *bondStretchInstance = new BondStretchInstance();
              bondStretchInstance->idx = n;
              rdkLineStream >> skip >> skip >> skip >> skip >>
                  bondStretchInstance->iAtomType >>
                  bondStretchInstance->jAtomType >>
                  bondStretchInstance->ffType >> skip >> skip >> skip >> skip >>
                  bondStretchInstance->kb;
              fixBondStretchInstance(bondStretchInstance);
              rdkBondStretchInstanceVec.push_back(bondStretchInstance);
              ++n;
            }
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            std::sort(rdkBondStretchInstanceVec.begin(),
                      rdkBondStretchInstanceVec.end(),
                      sortBondStretchInstanceVec);
            found =
                fgrep(rdkFStream, "TOTAL BOND STRETCH ENERGY", energyString);
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            std::stringstream rdkBondStretchStringStream(energyString);
            rdkBondStretchStringStream >> skip >> skip >> skip >> skip >>
                skip >> rdkEnergy;
            found = fgrep(refFStream, "B O N D   S T R E T C H I N G");
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            found = fgrep(refFStream, "----------------");
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            found = false;
            refBondStretchInstanceVec.clear();
            refPos = refFStream.tellg();
            n = 0;
            while ((!found) && (!(std::getline(refFStream, refLine).rdstate() &
                                  std::ifstream::failbit))) {
              found = (!(refLine.length()));
              if (found) {
                break;
              }
              std::stringstream refLineStream(refLine);
              auto *bondStretchInstance = new BondStretchInstance();
              bondStretchInstance->idx = n;
              refLineStream >> skip >> skip >> skip >> skip >>
                  bondStretchInstance->iAtomType >>
                  bondStretchInstance->jAtomType >>
                  bondStretchInstance->ffType >> skip >> skip >> skip >> skip >>
                  bondStretchInstance->kb;
              fixBondStretchInstance(bondStretchInstance);
              refBondStretchInstanceVec.push_back(bondStretchInstance);
              ++n;
            }
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            std::sort(refBondStretchInstanceVec.begin(),
                      refBondStretchInstanceVec.end(),
                      sortBondStretchInstanceVec);
            found = fgrep(refFStream, "TOTAL BOND STRAIN ENERGY", energyString);
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            std::stringstream refBondStretchStringStream(energyString);
            refBondStretchStringStream >> skip >> skip >> skip >> skip >>
                skip >> refEnergy;
            error = (rdkBondStretchInstanceVec.size() !=
                     refBondStretchInstanceVec.size());
            if (error) {
              failed = true;
              std::stringstream diff;
              diff << "Bond stretching: expected "
                   << refBondStretchInstanceVec.size()
                   << " interactions, found "
                   << rdkBondStretchInstanceVec.size() << "\n";
              errorMsg += diff.str();
            }
            for (unsigned j = 0;
                 (!error) & (j < rdkBondStretchInstanceVec.size()); ++j) {
              error =
                  ((rdkBondStretchInstanceVec[j]->iAtomType !=
                    refBondStretchInstanceVec[j]->iAtomType) ||
                   (rdkBondStretchInstanceVec[j]->jAtomType !=
                    refBondStretchInstanceVec[j]->jAtomType) ||
                   (rdkBondStretchInstanceVec[j]->ffType !=
                    refBondStretchInstanceVec[j]->ffType) ||
                   (fabs(rdkBondStretchInstanceVec[j]->kb -
                         refBondStretchInstanceVec[j]->kb) > FCON_TOLERANCE));
              if (error) {
                failed = true;
                bool haveLine;
                haveLine =
                    getLineByNum(rdkFStream, rdkPos,
                                 rdkBondStretchInstanceVec[j]->idx, rdkLine);
                if (haveLine) {
                  haveLine =
                      getLineByNum(refFStream, refPos,
                                   refBondStretchInstanceVec[j]->idx, refLine);
                }
                std::stringstream diff;
                diff << "Bond stretching: found a difference\n";
                if (haveLine) {
                  diff << "Expected:\n"
                       << rdkLine << "\nFound:\n"
                       << refLine << "\n";
                }
                errorMsg += diff.str();
              }
              delete rdkBondStretchInstanceVec[j];
              delete refBondStretchInstanceVec[j];
            }
            error = (fabs(rdkEnergy - refEnergy) > ENERGY_TOLERANCE);
            if (error && checkEnergy) {
              failed = true;
              std::stringstream diff;
              diff << "Bond stretching: energies differ\n"
                      "Expected "
                   << std::fixed << std::setprecision(4) << refEnergy
                   << ", found " << std::fixed << std::setprecision(4)
                   << rdkEnergy << "\n";
              errorMsg += diff.str();
            }
          }

          // --------------------------------------------------
          // A N G L E   B E N D I N G
          // --------------------------------------------------
          if (refHaveAngleBend) {
            corruptedMsg = ": Angle bending: corrupted input data\n";
            found = fgrep(rdkFStream, "A N G L E   B E N D I N G");
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            found = fgrep(rdkFStream, "----------------");
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            found = false;
            rdkAngleBendInstanceVec.clear();
            rdkPos = rdkFStream.tellg();
            n = 0;
            while ((!found) && (!(std::getline(rdkFStream, rdkLine).rdstate() &
                                  std::ifstream::failbit))) {
              found = (!(rdkLine.length()));
              if (found) {
                break;
              }
              std::stringstream rdkLineStream(rdkLine);
              auto *angleBendInstance = new AngleBendInstance();
              angleBendInstance->idx = n;
              rdkLineStream >> skip >> skip >> skip >> skip >> skip >> skip >>
                  angleBendInstance->iAtomType >>
                  angleBendInstance->jAtomType >>
                  angleBendInstance->kAtomType >> angleBendInstance->ffType >>
                  skip >> skip >> skip >> skip >> angleBendInstance->ka;
              fixAngleBendInstance(angleBendInstance);
              rdkAngleBendInstanceVec.push_back(angleBendInstance);
              ++n;
            }
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            std::sort(rdkAngleBendInstanceVec.begin(),
                      rdkAngleBendInstanceVec.end(), sortAngleBendInstanceVec);
            found = fgrep(rdkFStream, "TOTAL ANGLE BEND ENERGY", energyString);
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            std::stringstream rdkAngleBendStringStream(energyString);
            rdkAngleBendStringStream >> skip >> skip >> skip >> skip >> skip >>
                rdkEnergy;
            found = fgrep(refFStream, "A N G L E   B E N D I N G");
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            found = fgrep(refFStream, "----------------");
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            found = false;
            refAngleBendInstanceVec.clear();
            refPos = refFStream.tellg();
            n = 0;
            while ((!found) && (!(std::getline(refFStream, refLine).rdstate() &
                                  std::ifstream::failbit))) {
              found = (!(refLine.length()));
              if (found) {
                break;
              }
              std::stringstream refLineStream(refLine);
              auto *angleBendInstance = new AngleBendInstance();
              angleBendInstance->idx = n;
              refLineStream >> skip >> skip >> skip >> skip >>
                  angleBendInstance->iAtomType >>
                  angleBendInstance->jAtomType >>
                  angleBendInstance->kAtomType >> angleBendInstance->ffType >>
                  skip >> skip >> skip >> skip >> angleBendInstance->ka;
              fixAngleBendInstance(angleBendInstance);
              refAngleBendInstanceVec.push_back(angleBendInstance);
              ++n;
            }
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            std::sort(refAngleBendInstanceVec.begin(),
                      refAngleBendInstanceVec.end(), sortAngleBendInstanceVec);
            found =
                fgrep(refFStream, "TOTAL ANGLE STRAIN ENERGY", energyString);
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            std::stringstream refAngleBendStringStream(energyString);
            refAngleBendStringStream >> skip >> skip >> skip >> skip >> skip >>
                refEnergy;
            error = (rdkAngleBendInstanceVec.size() !=
                     refAngleBendInstanceVec.size());
            if (error) {
              failed = true;
              std::stringstream diff;
              diff << "Angle bending: expected "
                   << refAngleBendInstanceVec.size() << " interactions, found "
                   << rdkAngleBendInstanceVec.size() << "\n";
              errorMsg += diff.str();
            }
            for (unsigned j = 0;
                 (!error) & (j < rdkAngleBendInstanceVec.size()); ++j) {
              error = ((rdkAngleBendInstanceVec[j]->iAtomType !=
                        refAngleBendInstanceVec[j]->iAtomType) ||
                       (rdkAngleBendInstanceVec[j]->jAtomType !=
                        refAngleBendInstanceVec[j]->jAtomType) ||
                       (rdkAngleBendInstanceVec[j]->kAtomType !=
                        refAngleBendInstanceVec[j]->kAtomType) ||
                       (rdkAngleBendInstanceVec[j]->ffType !=
                        refAngleBendInstanceVec[j]->ffType) ||
                       (fabs(rdkAngleBendInstanceVec[j]->ka -
                             refAngleBendInstanceVec[j]->ka) > FCON_TOLERANCE));
              if (error) {
                failed = true;
                bool haveLine;
                haveLine =
                    getLineByNum(rdkFStream, rdkPos,
                                 rdkAngleBendInstanceVec[j]->idx, rdkLine);
                if (haveLine) {
                  haveLine =
                      getLineByNum(refFStream, refPos,
                                   refAngleBendInstanceVec[j]->idx, refLine);
                }
                std::stringstream diff;
                diff << "Angle bending: found a difference\n";
                if (haveLine) {
                  diff << "Expected:\n"
                       << rdkLine << "\nFound:\n"
                       << refLine << "\n";
                }
                errorMsg += diff.str();
              }
              delete rdkAngleBendInstanceVec[j];
              delete refAngleBendInstanceVec[j];
            }
            error = (fabs(rdkEnergy - refEnergy) > ENERGY_TOLERANCE);
            if (error && checkEnergy) {
              failed = true;
              std::stringstream diff;
              diff << "Angle bending: energies differ\n"
                      "Expected "
                   << std::fixed << std::setprecision(4) << refEnergy
                   << ", found " << std::fixed << std::setprecision(4)
                   << rdkEnergy << "\n";
              errorMsg += diff.str();
            }
          }

          // --------------------------------------------------
          // S T R E T C H   B E N D I N G
          // --------------------------------------------------
          if (refHaveStretchBend) {
            corruptedMsg = ": Stretch-bending: corrupted input data\n";
            found = fgrep(rdkFStream, "S T R E T C H   B E N D I N G");
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            found = fgrep(rdkFStream, "----------------");
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            found = false;
            rdkStretchBendInstanceVec.clear();
            rdkPos = rdkFStream.tellg();
            n = 0;
            while ((!found) && (!(std::getline(rdkFStream, rdkLine).rdstate() &
                                  std::ifstream::failbit))) {
              found = (!(rdkLine.length()));
              if (found) {
                break;
              }
              std::stringstream rdkLineStream(rdkLine);
              auto *stretchBendInstance = new StretchBendInstance();
              stretchBendInstance->idx = n;
              rdkLineStream >> skip >> skip >> skip >> skip >> skip >> skip >>
                  stretchBendInstance->iAtomType >>
                  stretchBendInstance->jAtomType >>
                  stretchBendInstance->kAtomType >>
                  stretchBendInstance->ffType >> skip >> skip >> skip >> skip >>
                  skip >> stretchBendInstance->kba;
              rdkStretchBendInstanceVec.push_back(stretchBendInstance);
              ++n;
            }
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            std::sort(rdkStretchBendInstanceVec.begin(),
                      rdkStretchBendInstanceVec.end(),
                      sortStretchBendInstanceVec);
            found =
                fgrep(rdkFStream, "TOTAL STRETCH-BEND ENERGY", energyString);
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            std::stringstream rdkStretchBendStringStream(energyString);
            rdkStretchBendStringStream >> skip >> skip >> skip >> skip >>
                rdkEnergy;
            found = fgrep(refFStream, "S T R E T C H   B E N D I N G");
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            found = fgrep(refFStream, "----------------");
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            found = false;
            refStretchBendInstanceVec.clear();
            refPos = refFStream.tellg();
            n = 0;
            while ((!found) && (!(std::getline(refFStream, refLine).rdstate() &
                                  std::ifstream::failbit))) {
              found = (!(refLine.length()));
              if (found) {
                break;
              }
              std::stringstream refLineStream(refLine);
              auto *stretchBendInstance = new StretchBendInstance();
              stretchBendInstance->idx = n;
              refLineStream >> skip >> skip >> skip >> skip >>
                  stretchBendInstance->iAtomType >>
                  stretchBendInstance->jAtomType >>
                  stretchBendInstance->kAtomType >>
                  stretchBendInstance->ffType >> skip >> skip >> skip >> skip >>
                  stretchBendInstance->kba;
              refStretchBendInstanceVec.push_back(stretchBendInstance);
              ++n;
            }
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            std::sort(refStretchBendInstanceVec.begin(),
                      refStretchBendInstanceVec.end(),
                      sortStretchBendInstanceVec);
            found = fgrep(refFStream, "TOTAL STRETCH-BEND STRAIN ENERGY",
                          energyString);
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            std::stringstream refStretchBendStringStream(energyString);
            refStretchBendStringStream >> skip >> skip >> skip >> skip >>
                skip >> refEnergy;
            error = (rdkStretchBendInstanceVec.size() !=
                     refStretchBendInstanceVec.size());
            if (error) {
              failed = true;
              std::stringstream diff;
              diff << "Stretch-bending: expected "
                   << refStretchBendInstanceVec.size()
                   << " interactions, found "
                   << rdkStretchBendInstanceVec.size() << "\n";
              errorMsg += diff.str();
            }
            for (unsigned j = 0;
                 (!error) & (j < rdkStretchBendInstanceVec.size()); ++j) {
              error =
                  ((rdkStretchBendInstanceVec[j]->iAtomType !=
                    refStretchBendInstanceVec[j]->iAtomType) ||
                   (rdkStretchBendInstanceVec[j]->jAtomType !=
                    refStretchBendInstanceVec[j]->jAtomType) ||
                   (rdkStretchBendInstanceVec[j]->kAtomType !=
                    refStretchBendInstanceVec[j]->kAtomType) ||
                   (rdkStretchBendInstanceVec[j]->ffType !=
                    refStretchBendInstanceVec[j]->ffType) ||
                   (fabs(rdkStretchBendInstanceVec[j]->kba -
                         refStretchBendInstanceVec[j]->kba) > FCON_TOLERANCE));
              if (error) {
                failed = true;
                bool haveLine;
                haveLine =
                    getLineByNum(rdkFStream, rdkPos,
                                 rdkStretchBendInstanceVec[j]->idx, rdkLine);
                if (haveLine) {
                  haveLine =
                      getLineByNum(refFStream, refPos,
                                   refStretchBendInstanceVec[j]->idx, refLine);
                }
                std::stringstream diff;
                diff << "Stretch-bending: found a difference\n";
                if (haveLine) {
                  diff << "Expected:\n"
                       << rdkLine << "\nFound:\n"
                       << refLine << "\n";
                }
                errorMsg += diff.str();
              }
              delete rdkStretchBendInstanceVec[j];
              delete refStretchBendInstanceVec[j];
            }
            error = (fabs(rdkEnergy - refEnergy) > ENERGY_TOLERANCE);
            if (error && checkEnergy) {
              failed = true;
              std::stringstream diff;
              diff << "Stretch-bending: energies differ\n"
                      "Expected "
                   << std::fixed << std::setprecision(4) << refEnergy
                   << ", found " << std::fixed << std::setprecision(4)
                   << rdkEnergy << "\n";
              errorMsg += diff.str();
            }
          }

          // --------------------------------------------------
          // O U T - O F - P L A N E   B E N D I N G
          // --------------------------------------------------
          if (refHaveOopBend) {
            corruptedMsg = ": Out-of-plane bending: corrupted input data\n";
            found =
                fgrep(rdkFStream, "O U T - O F - P L A N E   B E N D I N G");
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            found = fgrep(rdkFStream, "----------------");
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            found = false;
            rdkOopBendInstanceVec.clear();
            rdkPos = rdkFStream.tellg();
            n = 0;
            while ((!found) && (!(std::getline(rdkFStream, rdkLine).rdstate() &
                                  std::ifstream::failbit))) {
              found = (!(rdkLine.length()));
              if (found) {
                break;
              }
              std::stringstream rdkLineStream(rdkLine);
              auto *oopBendInstance = new OopBendInstance();
              oopBendInstance->idx = n;
              rdkLineStream >> skip >> skip >> skip >> skip >> skip >> skip >>
                  skip >> skip >> oopBendInstance->iAtomType >>
                  oopBendInstance->jAtomType >> oopBendInstance->kAtomType >>
                  oopBendInstance->lAtomType >> skip >> skip >>
                  oopBendInstance->koop;
              fixOopBendInstance(oopBendInstance);
              rdkOopBendInstanceVec.push_back(oopBendInstance);
              ++n;
            }
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            std::sort(rdkOopBendInstanceVec.begin(),
                      rdkOopBendInstanceVec.end(), sortOopBendInstanceVec);
            found = fgrep(rdkFStream, "TOTAL OUT-OF-PLANE BEND ENERGY",
                          energyString);
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            std::stringstream rdkOopBendStringStream(energyString);
            rdkOopBendStringStream >> skip >> skip >> skip >> skip >> skip >>
                rdkEnergy;
            found =
                fgrep(refFStream, "O U T - O F - P L A N E    B E N D I N G");
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            found = fgrep(refFStream, "----------------");
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            found = false;
            refOopBendInstanceVec.clear();
            refPos = refFStream.tellg();
            n = 0;
            while ((!found) && (!(std::getline(refFStream, refLine).rdstate() &
                                  std::ifstream::failbit))) {
              found = (!(refLine.length()));
              if (found) {
                break;
              }
              std::stringstream refLineStream(refLine);
              auto *oopBendInstance = new OopBendInstance();
              oopBendInstance->idx = n;
              refLineStream >> skip >> skip >> skip >> skip >> skip >>
                  oopBendInstance->iAtomType >> oopBendInstance->jAtomType >>
                  oopBendInstance->kAtomType >> oopBendInstance->lAtomType >>
                  skip >> skip >> oopBendInstance->koop;
              fixOopBendInstance(oopBendInstance);
              refOopBendInstanceVec.push_back(oopBendInstance);
              ++n;
            }
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            std::sort(refOopBendInstanceVec.begin(),
                      refOopBendInstanceVec.end(), sortOopBendInstanceVec);
            found = fgrep(refFStream, "TOTAL OUT-OF-PLANE STRAIN ENERGY",
                          energyString);
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            std::stringstream refOopBendStringStream(energyString);
            refOopBendStringStream >> skip >> skip >> skip >> skip >> skip >>
                refEnergy;
            error =
                (rdkOopBendInstanceVec.size() != refOopBendInstanceVec.size());
            if (error) {
              failed = true;
              std::stringstream diff;
              diff << "Out-of-plane bending: expected "
                   << refOopBendInstanceVec.size() << " interactions, found "
                   << rdkOopBendInstanceVec.size() << "\n";
              errorMsg += diff.str();
            }
            for (unsigned j = 0; (!error) & (j < rdkOopBendInstanceVec.size());
                 ++j) {
              error = ((rdkOopBendInstanceVec[j]->iAtomType !=
                        refOopBendInstanceVec[j]->iAtomType) ||
                       (rdkOopBendInstanceVec[j]->jAtomType !=
                        refOopBendInstanceVec[j]->jAtomType) ||
                       (rdkOopBendInstanceVec[j]->kAtomType !=
                        refOopBendInstanceVec[j]->kAtomType) ||
                       (rdkOopBendInstanceVec[j]->lAtomType !=
                        refOopBendInstanceVec[j]->lAtomType) ||
                       (fabs(rdkOopBendInstanceVec[j]->koop -
                             refOopBendInstanceVec[j]->koop) > FCON_TOLERANCE));
              if (error) {
                failed = true;
                bool haveLine;
                haveLine = getLineByNum(rdkFStream, rdkPos,
                                        rdkOopBendInstanceVec[j]->idx, rdkLine);
                if (haveLine) {
                  haveLine =
                      getLineByNum(refFStream, refPos,
                                   refOopBendInstanceVec[j]->idx, refLine);
                }
                std::stringstream diff;
                diff << "Out-of-plane bending: found a difference\n";
                if (haveLine) {
                  diff << "Expected:\n"
                       << rdkLine << "\nFound:\n"
                       << refLine << "\n";
                }
                errorMsg += diff.str();
              }
              delete rdkOopBendInstanceVec[j];
              delete refOopBendInstanceVec[j];
            }
            error = (fabs(rdkEnergy - refEnergy) > ENERGY_TOLERANCE);
            if (error && checkEnergy) {
              failed = true;
              std::stringstream diff;
              diff << "Out-of-plane bending: energies differ\n"
                      "Expected "
                   << std::fixed << std::setprecision(4) << refEnergy
                   << ", found " << std::fixed << std::setprecision(4)
                   << rdkEnergy << "\n";
              errorMsg += diff.str();
            }
          }

          // --------------------------------------------------
          // T O R S I O N A L
          // --------------------------------------------------
          if (refHaveTorsion) {
            corruptedMsg = ": Torsional: corrupted input data\n";
            found = fgrep(rdkFStream, "T O R S I O N A L");
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            found = fgrep(rdkFStream, "----------------");
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            found = false;
            rdkTorsionInstanceVec.clear();
            rdkPos = rdkFStream.tellg();
            n = 0;
            while ((!found) && (!(std::getline(rdkFStream, rdkLine).rdstate() &
                                  std::ifstream::failbit))) {
              found = (!(rdkLine.length()));
              if (found) {
                break;
              }
              std::stringstream rdkLineStream(rdkLine);
              auto *torsionInstance = new TorsionInstance();
              torsionInstance->idx = n;
              rdkLineStream >> skip >> skip >> skip >> skip >> skip >> skip >>
                  skip >> skip >> torsionInstance->iAtomType >>
                  torsionInstance->jAtomType >> torsionInstance->kAtomType >>
                  torsionInstance->lAtomType >> torsionInstance->ffType >>
                  skip >> skip >> torsionInstance->V1 >> torsionInstance->V2 >>
                  torsionInstance->V3;
              fixTorsionInstance(torsionInstance);
              rdkTorsionInstanceVec.push_back(torsionInstance);
              ++n;
            }
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            std::sort(rdkTorsionInstanceVec.begin(),
                      rdkTorsionInstanceVec.end(), sortTorsionInstanceVec);
            found = fgrep(rdkFStream, "TOTAL TORSIONAL ENERGY", energyString);
            if (!found) {
              errorMsg += rdk + corruptedMsg;
              break;
            }
            std::stringstream rdkTorsionStringStream(energyString);
            rdkTorsionStringStream >> skip >> skip >> skip >> skip >> rdkEnergy;
            found = fgrep(refFStream, "T O R S I O N A L");
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            found = fgrep(refFStream, "----------------");
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            found = false;
            refTorsionInstanceVec.clear();
            refPos = refFStream.tellg();
            n = 0;
            double refTorsionEnergy;
            while ((!found) && (!(std::getline(refFStream, refLine).rdstate() &
                                  std::ifstream::failbit))) {
              found = (!(refLine.length()));
              if (found) {
                break;
              }
              std::stringstream refLineStream(refLine);
              auto *torsionInstance = new TorsionInstance();
              torsionInstance->idx = n;
              refLineStream >> skip >> skip >> skip >> skip >> skip >> skip >>
                  torsionInstance->iAtomType >> torsionInstance->jAtomType >>
                  torsionInstance->kAtomType >> torsionInstance->lAtomType >>
                  torsionInstance->ffType >> skip >> refTorsionEnergy >>
                  torsionInstance->V1 >> torsionInstance->V2 >>
                  torsionInstance->V3;
              if (!(ForceFields::MMFF::isDoubleZero(torsionInstance->V1) &&
                    ForceFields::MMFF::isDoubleZero(torsionInstance->V2) &&
                    ForceFields::MMFF::isDoubleZero(torsionInstance->V3) &&
                    ForceFields::MMFF::isDoubleZero(refTorsionEnergy))) {
                fixTorsionInstance(torsionInstance);
                refTorsionInstanceVec.push_back(torsionInstance);
              } else {
                delete torsionInstance;
              }
              ++n;
            }
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            std::sort(refTorsionInstanceVec.begin(),
                      refTorsionInstanceVec.end(), sortTorsionInstanceVec);
            found =
                fgrep(refFStream, "TOTAL TORSION STRAIN ENERGY", energyString);
            if (!found) {
              errorMsg += ref + corruptedMsg;
              break;
            }
            std::stringstream refTorsionStringStream(energyString);
            refTorsionStringStream >> skip >> skip >> skip >> skip >> skip >>
                refEnergy;
            error =
                (rdkTorsionInstanceVec.size() != refTorsionInstanceVec.size());
            if (error) {
              failed = true;
              std::stringstream diff;
              diff << "Torsional: expected " << refTorsionInstanceVec.size()
                   << " interactions, found " << rdkTorsionInstanceVec.size()
                   << "\n";
              errorMsg += diff.str();
            }
            for (unsigned j = 0; (!error) & (j < rdkTorsionInstanceVec.size());
                 ++j) {
              error = ((rdkTorsionInstanceVec[j]->iAtomType !=
                        refTorsionInstanceVec[j]->iAtomType) ||
                       (rdkTorsionInstanceVec[j]->jAtomType !=
                        refTorsionInstanceVec[j]->jAtomType) ||
                       (rdkTorsionInstanceVec[j]->kAtomType !=
                        refTorsionInstanceVec[j]->kAtomType) ||
                       (rdkTorsionInstanceVec[j]->lAtomType !=
                        refTorsionInstanceVec[j]->lAtomType) ||
                       (rdkTorsionInstanceVec[j]->ffType !=
                        refTorsionInstanceVec[j]->ffType) ||
                       (fabs(rdkTorsionInstanceVec[j]->V1 -
                             refTorsionInstanceVec[j]->V1) > FCON_TOLERANCE) ||
                       (fabs(rdkTorsionInstanceVec[j]->V2 -
                             refTorsionInstanceVec[j]->V2) > FCON_TOLERANCE) ||
                       (fabs(rdkTorsionInstanceVec[j]->V3 -
                             refTorsionInstanceVec[j]->V3) > FCON_TOLERANCE));
              if (error) {
                failed = true;
                bool haveLine;
                haveLine = getLineByNum(rdkFStream, rdkPos,
                                        rdkTorsionInstanceVec[j]->idx, rdkLine);
                if (haveLine) {
                  haveLine =
                      getLineByNum(refFStream, refPos,
                                   refTorsionInstanceVec[j]->idx, refLine);
                }
                std::stringstream diff;
                diff << "Torsional: found a difference\n";
                if (haveLine) {
                  diff << "Expected:\n"
                       << rdkLine << "\nFound:\n"
                       << refLine << "\n";
                }
                errorMsg += diff.str();
              }
            }
            for (auto &ti : rdkTorsionInstanceVec) { delete ti; }
            for (auto &ti : refTorsionInstanceVec) { delete ti; }
            error = (fabs(rdkEnergy - refEnergy) > ENERGY_TOLERANCE);
            if (error && checkEnergy) {
              failed = true;
              std::stringstream diff;
              diff << "Torsional: energies differ\n"
                      "Expected "
                   << std::fixed << std::setprecision(4) << refEnergy
                   << ", found " << std::fixed << std::setprecision(4)
                   << rdkEnergy << "\n";
              errorMsg += diff.str();
            }
          }

          // --------------------------------------------------
          // V A N   D E R   W A A L S
          // E L E C T R O S T A T I C
          // --------------------------------------------------
          corruptedMsg = ": Van der Waals: corrupted input data\n";
          found = fgrep(rdkFStream, "V A N   D E R   W A A L S");
          if (!found) {
            errorMsg += rdk + corruptedMsg;
            break;
          }
          found = fgrep(rdkFStream, "----------------");
          if (!found) {
            errorMsg += rdk + corruptedMsg;
            break;
          }
          found = fgrep(rdkFStream, "TOTAL VAN DER WAALS ENERGY", energyString);
          if (!found) {
            errorMsg += rdk + corruptedMsg;
            break;
          }
          if (!refHaveVdW) {
            errorMsg += ref + corruptedMsg;
            break;
          }
          std::stringstream rdkVdWStringStream(energyString);
          rdkVdWStringStream >> skip >> skip >> skip >> skip >> skip >> skip >>
              rdkEnergy;
          corruptedMsg = ": Electrostatic: corrupted input data";
          found = fgrep(rdkFStream, "E L E C T R O S T A T I C");
          if (!found) {
            errorMsg += rdk + corruptedMsg;
            break;
          }
          found = fgrep(rdkFStream, "----------------");
          if (!found) {
            errorMsg += rdk + corruptedMsg;
            break;
          }
          found = fgrep(rdkFStream, "TOTAL ELECTROSTATIC ENERGY", energyString);
          if (!found) {
            errorMsg += rdk + corruptedMsg;
            break;
          }
          if (!refHaveEle) {
            errorMsg += ref + corruptedMsg;
            break;
          }
          std::stringstream rdkEleStringStream(energyString);
          rdkEleStringStream >> skip >> skip >> skip >> skip >> rdkEleEnergy;
          error = (fabs(rdkEnergy - refVdWEnergy) > ENERGY_TOLERANCE);
          if (error && checkEnergy) {
            failed = true;
            std::stringstream diff;
            diff << "Van der Waals: energies differ\n"
                    "Expected "
                 << std::fixed << std::setprecision(4) << refVdWEnergy
                 << ", found " << std::fixed << std::setprecision(4)
                 << rdkEnergy << "\n";
            errorMsg += diff.str();
          }
          error = (fabs(rdkEleEnergy - refEleEnergy) > ENERGY_TOLERANCE);
          if (error && checkEnergy) {
            failed = true;
            std::stringstream diff;
            diff << "Electrostatic: energies differ\n"
                    "Expected "
                 << std::fixed << std::setprecision(4) << refEleEnergy
                 << ", found " << std::fixed << std::setprecision(4)
                 << rdkEnergy << "\n";
            errorMsg += diff.str();
          }
          if (failed) {
            std::cerr << nameArray[ii] << "\n\n" << errorMsg << std::endl;
            ++n_failed;
          }
        }
        if (!found) {
          if (!failed) {
            std::cerr << nameArray[ii] << "\n\n";
          }
          std::cerr << errorMsg << std::endl;
        } else {
          unsigned int n_passed = nameArray.size() - n_failed;
          std::cerr << (n_failed ? "" : "All ") << n_passed << " test"
                    << ((n_passed == 1) ? "" : "s") << " passed" << std::endl;
          if (n_failed) {
            std::cerr << n_failed << " test" << ((n_failed == 1) ? "" : "s")
                      << " failed" << std::endl;
          }
        }
        if (n_failed) {
          testFailure = true;
        }
        rdkFStream.close();
        refFStream.close();
        firstTest = false;
      }
    }
  }

  TEST_ASSERT(!testFailure);
  std::cerr << "  done" << std::endl;
  return 0;
}

void testMMFFAllConstraints() {
  std::cerr << "-------------------------------------" << std::endl;
  std::cerr << "Unit tests for all MMFF constraint terms." << std::endl;

  std::string molBlock =
      "butane\n"
      "     RDKit          3D\n"
      "butane\n"
      " 17 16  0  0  0  0  0  0  0  0999 V2000\n"
      "    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    1.4280    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    1.7913   -0.2660    0.9927 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    1.9040    1.3004   -0.3485 C   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    1.5407    2.0271    0.3782 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    1.5407    1.5664   -1.3411 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    3.3320    1.3004   -0.3485 C   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    3.6953    1.5162   -1.3532 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    3.8080    0.0192    0.0649 C   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    3.4447   -0.7431   -0.6243 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    3.4447   -0.1966    1.0697 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    4.8980    0.0192    0.0649 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    3.6954    2.0627    0.3408 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    1.7913   -0.7267   -0.7267 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "   -0.3633    0.7267    0.7267 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "   -0.3633   -0.9926    0.2660 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "   -0.3633    0.2660   -0.9926 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "  1  2  1  0  0  0  0\n"
      "  1 15  1  0  0  0  0\n"
      "  1 16  1  0  0  0  0\n"
      "  1 17  1  0  0  0  0\n"
      "  2  3  1  0  0  0  0\n"
      "  2  4  1  0  0  0  0\n"
      "  2 14  1  0  0  0  0\n"
      "  4  5  1  0  0  0  0\n"
      "  4  6  1  0  0  0  0\n"
      "  4  7  1  0  0  0  0\n"
      "  7  8  1  0  0  0  0\n"
      "  7  9  1  0  0  0  0\n"
      "  7 13  1  0  0  0  0\n"
      "  9 10  1  0  0  0  0\n"
      "  9 11  1  0  0  0  0\n"
      "  9 12  1  0  0  0  0\n"
      "M  END\n";
  RDKit::RWMol *mol;
  ForceFields::ForceField *field;
  MMFF::MMFFMolProperties *mmffMolProperties;

  // distance constraints
  ForceFields::MMFF::DistanceConstraintContrib *dc;
  mol = RDKit::MolBlockToMol(molBlock, true, false);
  TEST_ASSERT(mol);
  MolTransforms::setBondLength(mol->getConformer(), 1, 3, 2.0);
  mmffMolProperties = new MMFF::MMFFMolProperties(*mol);
  TEST_ASSERT(mmffMolProperties);
  TEST_ASSERT(mmffMolProperties->isValid());
  field = RDKit::MMFF::constructForceField(*mol, mmffMolProperties);
  TEST_ASSERT(field);
  field->initialize();
  dc = new ForceFields::MMFF::DistanceConstraintContrib(field, 1, 3, 2.0, 2.0,
                                                        1.0e5);
  field->contribs().push_back(ForceFields::ContribPtr(dc));
  field->minimize();
  TEST_ASSERT(RDKit::feq(
      MolTransforms::getBondLength(mol->getConformer(), 1, 3), 2.0, 0.1));
  delete field;
  delete mmffMolProperties;
  mmffMolProperties = new MMFF::MMFFMolProperties(*mol);
  TEST_ASSERT(mmffMolProperties);
  TEST_ASSERT(mmffMolProperties->isValid());
  field = RDKit::MMFF::constructForceField(*mol, mmffMolProperties);
  field->initialize();
  dc = new ForceFields::MMFF::DistanceConstraintContrib(field, 1, 3, true, -0.2,
                                                        0.2, 1.0e5);
  field->contribs().push_back(ForceFields::ContribPtr(dc));
  field->minimize();
  TEST_ASSERT(MolTransforms::getBondLength(mol->getConformer(), 1, 3) > 1.79);
  delete field;
  delete mmffMolProperties;
  delete mol;

  // angle constraints
  ForceFields::MMFF::AngleConstraintContrib *ac;
  mol = RDKit::MolBlockToMol(molBlock, true, false);
  TEST_ASSERT(mol);
  MolTransforms::setAngleDeg(mol->getConformer(), 1, 3, 6, 90.0);
  mmffMolProperties = new MMFF::MMFFMolProperties(*mol);
  TEST_ASSERT(mmffMolProperties);
  TEST_ASSERT(mmffMolProperties->isValid());
  field = RDKit::MMFF::constructForceField(*mol, mmffMolProperties);
  TEST_ASSERT(field);
  field->initialize();
  ac = new ForceFields::MMFF::AngleConstraintContrib(field, 1, 3, 6, 90.0, 90.0,
                                                     1.0e5);
  field->contribs().push_back(ForceFields::ContribPtr(ac));
  field->minimize();
  TEST_ASSERT(RDKit::feq(
      MolTransforms::getAngleDeg(mol->getConformer(), 1, 3, 6), 90.0, 0.5));
  delete field;
  delete mmffMolProperties;
  mmffMolProperties = new MMFF::MMFFMolProperties(*mol);
  TEST_ASSERT(mmffMolProperties);
  TEST_ASSERT(mmffMolProperties->isValid());
  field = RDKit::MMFF::constructForceField(*mol, mmffMolProperties);
  field->initialize();
  ac = new ForceFields::MMFF::AngleConstraintContrib(field, 1, 3, 6, true,
                                                     -10.0, 10.0, 1.0e5);
  field->contribs().push_back(ForceFields::ContribPtr(ac));
  field->minimize();
  TEST_ASSERT(RDKit::feq(
      MolTransforms::getAngleDeg(mol->getConformer(), 1, 3, 6), 100.0, 0.5));
  delete field;
  delete mmffMolProperties;
  MolTransforms::setAngleDeg(mol->getConformer(), 1, 3, 6, 0.0);
  mmffMolProperties = new MMFF::MMFFMolProperties(*mol);
  TEST_ASSERT(mmffMolProperties);
  TEST_ASSERT(mmffMolProperties->isValid());
  field = RDKit::MMFF::constructForceField(*mol, mmffMolProperties);
  field->initialize();
  ac = new ForceFields::MMFF::AngleConstraintContrib(field, 1, 3, 6, false,
                                                     -10.0, 10.0, 1.0e5);
  field->contribs().push_back(ForceFields::ContribPtr(ac));
  field->minimize();
  TEST_ASSERT(RDKit::feq(
      MolTransforms::getAngleDeg(mol->getConformer(), 1, 3, 6), 10.0, 0.5));
  delete field;
  delete mmffMolProperties;
  delete mol;

  // torsion constraints
  ForceFields::MMFF::TorsionConstraintContrib *tc;
  mol = RDKit::MolBlockToMol(molBlock, true, false);
  TEST_ASSERT(mol);
  MolTransforms::setDihedralDeg(mol->getConformer(), 1, 3, 6, 8, 15.0);
  mmffMolProperties = new MMFF::MMFFMolProperties(*mol);
  TEST_ASSERT(mmffMolProperties);
  TEST_ASSERT(mmffMolProperties->isValid());
  field = RDKit::MMFF::constructForceField(*mol, mmffMolProperties);
  TEST_ASSERT(field);
  field->initialize();
  tc = new ForceFields::MMFF::TorsionConstraintContrib(field, 1, 3, 6, 8, 10.0,
                                                       20.0, 1.0e5);
  field->contribs().push_back(ForceFields::ContribPtr(tc));
  field->minimize();
  TEST_ASSERT(
      RDKit::feq(MolTransforms::getDihedralDeg(mol->getConformer(), 1, 3, 6, 8),
                 20.0, 0.5));
  delete field;
  delete mmffMolProperties;
  MolTransforms::setDihedralDeg(mol->getConformer(), 1, 3, 6, 8, -30.0);
  mmffMolProperties = new MMFF::MMFFMolProperties(*mol);
  TEST_ASSERT(mmffMolProperties);
  TEST_ASSERT(mmffMolProperties->isValid());
  field = RDKit::MMFF::constructForceField(*mol, mmffMolProperties);
  field->initialize();
  tc = new ForceFields::MMFF::TorsionConstraintContrib(field, 1, 3, 6, 8, true,
                                                       -10.0, 8.0, 1.0e5);
  field->contribs().push_back(ForceFields::ContribPtr(tc));
  field->minimize();
  TEST_ASSERT(
      RDKit::feq(MolTransforms::getDihedralDeg(mol->getConformer(), 1, 3, 6, 8),
                 -40.0, 0.5));
  delete field;
  delete mmffMolProperties;
  MolTransforms::setDihedralDeg(mol->getConformer(), 1, 3, 6, 8, -10.0);
  mmffMolProperties = new MMFF::MMFFMolProperties(*mol);
  TEST_ASSERT(mmffMolProperties);
  TEST_ASSERT(mmffMolProperties->isValid());
  field = RDKit::MMFF::constructForceField(*mol, mmffMolProperties);
  field->initialize();
  tc = new ForceFields::MMFF::TorsionConstraintContrib(field, 1, 3, 6, 8, false,
                                                       -10.0, 8.0, 1.0e5);
  field->contribs().push_back(ForceFields::ContribPtr(tc));
  field->minimize(500);
  TEST_ASSERT(
      RDKit::feq(MolTransforms::getDihedralDeg(mol->getConformer(), 1, 3, 6, 8),
                 -10.0, 0.5));
  delete field;
  delete mmffMolProperties;
  delete mol;

  // position constraints
  ForceFields::MMFF::PositionConstraintContrib *pc;
  mol = RDKit::MolBlockToMol(molBlock, true, false);
  TEST_ASSERT(mol);
  mmffMolProperties = new MMFF::MMFFMolProperties(*mol);
  TEST_ASSERT(mmffMolProperties);
  TEST_ASSERT(mmffMolProperties->isValid());
  field = RDKit::MMFF::constructForceField(*mol, mmffMolProperties);
  TEST_ASSERT(field);
  field->initialize();
  RDGeom::Point3D p = mol->getConformer().getAtomPos(1);
  pc = new ForceFields::MMFF::PositionConstraintContrib(field, 1, 0.3, 1.0e5);
  field->contribs().push_back(ForceFields::ContribPtr(pc));
  field->minimize();
  RDGeom::Point3D q = mol->getConformer().getAtomPos(1);
  TEST_ASSERT((p - q).length() < 0.3);
  delete field;
  delete mmffMolProperties;
  delete mol;

  // fixed atoms
  mol = RDKit::MolBlockToMol(molBlock, true, false);
  TEST_ASSERT(mol);
  field = RDKit::MMFF::constructForceField(*mol);
  TEST_ASSERT(field);
  field->initialize();
  RDGeom::Point3D fp = mol->getConformer().getAtomPos(1);
  field->fixedPoints().push_back(1);
  field->minimize();
  RDGeom::Point3D fq = mol->getConformer().getAtomPos(1);
  TEST_ASSERT((fp - fq).length() < 0.01);
  delete field;
  delete mol;

  std::cerr << "  done" << std::endl;
}

void testMMFFTorsionConstraints() {
  std::cerr << "-------------------------------------" << std::endl;
  std::cerr << "Unit test for MMFF TorsionConstraint close to 0 degrees." << std::endl;

  std::string molBlock = R"(
     RDKit          3D

 11 10  0  0  0  0  0  0  0  0999 V2000
   -1.6091   -0.3348   -0.0457 N   0  0  0  0  0  0  0  0  0  0  0  0
   -2.3935    0.2978   -0.1061 H   0  0  0  0  0  0  0  0  0  0  0  0
   -1.6708   -0.8686    0.8103 H   0  0  0  0  0  0  0  0  0  0  0  0
   -0.3560    0.4358   -0.0518 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.3583    1.1207    0.7968 H   0  0  0  0  0  0  0  0  0  0  0  0
   -0.2978    1.0122   -0.9759 H   0  0  0  0  0  0  0  0  0  0  0  0
    0.8573   -0.4893    0.0451 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.8947   -1.1355   -0.8327 H   0  0  0  0  0  0  0  0  0  0  0  0
    0.7774   -1.1052    0.9415 H   0  0  0  0  0  0  0  0  0  0  0  0
    2.0396    0.2763    0.1127 O   0  0  0  0  0  0  0  0  0  0  0  0
    2.1165    0.7905   -0.6941 H   0  0  0  0  0  0  0  0  0  0  0  0
  1  2  1  0  0  0  0
  1  3  1  0  0  0  0
  1  4  1  0  0  0  0
  4  5  1  0  0  0  0
  4  6  1  0  0  0  0
  4  7  1  0  0  0  0
  7  8  1  0  0  0  0
  7  9  1  0  0  0  0
  7 10  1  0  0  0  0
 10 11  1  0  0  0  0
M  END
)";
  RDKit::RWMol *mol;

  // torsion constraints
  mol = RDKit::MolBlockToMol(molBlock, true, false);
  TEST_ASSERT(mol);
  std::vector<double> e;
  for (double d: {0.0001, 0.0}) {
    MolTransforms::setDihedralDeg(mol->getConformer(), 0, 3, 6, 9, d);
    MMFF::MMFFMolProperties mmffMolProperties(*mol);
    TEST_ASSERT(mmffMolProperties.isValid());
    ForceFields::ForceField *field = RDKit::MMFF::constructForceField(*mol, &mmffMolProperties);
    TEST_ASSERT(field);
    field->initialize();
    auto *tc = new ForceFields::MMFF::TorsionConstraintContrib(field, 0, 3, 6,
                                                               9, d, d, 1.0e6);
    field->contribs().push_back(ForceFields::ContribPtr(tc));
    field->minimize();
    e.push_back(field->calcEnergy());
    TEST_ASSERT(
        RDKit::feq(MolTransforms::getDihedralDeg(mol->getConformer(), 0, 3, 6, 9),
                   d, 0.5));
    delete field;
  }
  TEST_ASSERT(RDKit::feq(e[0], e[1], 0.5));
  delete mol;

  std::cerr << "  done" << std::endl;
}

void testMMFFCopy() {
  std::cerr << "-------------------------------------" << std::endl;
  std::cerr << "Unit tests for copying MMFF ForceFields." << std::endl;

  std::string molBlock =
      "butane\n"
      "     RDKit          3D\n"
      "butane\n"
      " 17 16  0  0  0  0  0  0  0  0999 V2000\n"
      "    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    1.4280    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    1.7913   -0.2660    0.9927 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    1.9040    1.3004   -0.3485 C   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    1.5407    2.0271    0.3782 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    1.5407    1.5664   -1.3411 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    3.3320    1.3004   -0.3485 C   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    3.6953    1.5162   -1.3532 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    3.8080    0.0192    0.0649 C   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    3.4447   -0.7431   -0.6243 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    3.4447   -0.1966    1.0697 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    4.8980    0.0192    0.0649 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    3.6954    2.0627    0.3408 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "    1.7913   -0.7267   -0.7267 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "   -0.3633    0.7267    0.7267 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "   -0.3633   -0.9926    0.2660 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "   -0.3633    0.2660   -0.9926 H   0  0  0  0  0  0  0  0  0  0  0  0\n"
      "  1  2  1  0  0  0  0\n"
      "  1 15  1  0  0  0  0\n"
      "  1 16  1  0  0  0  0\n"
      "  1 17  1  0  0  0  0\n"
      "  2  3  1  0  0  0  0\n"
      "  2  4  1  0  0  0  0\n"
      "  2 14  1  0  0  0  0\n"
      "  4  5  1  0  0  0  0\n"
      "  4  6  1  0  0  0  0\n"
      "  4  7  1  0  0  0  0\n"
      "  7  8  1  0  0  0  0\n"
      "  7  9  1  0  0  0  0\n"
      "  7 13  1  0  0  0  0\n"
      "  9 10  1  0  0  0  0\n"
      "  9 11  1  0  0  0  0\n"
      "  9 12  1  0  0  0  0\n"
      "M  END\n";

  {
    RDKit::RWMol *mol = RDKit::MolBlockToMol(molBlock, true, false);
    TEST_ASSERT(mol);
    auto *cmol = new RDKit::RWMol(*mol);
    TEST_ASSERT(cmol);

    MMFF::MMFFMolProperties *mmffMolProperties =
        new MMFF::MMFFMolProperties(*mol);
    TEST_ASSERT(mmffMolProperties);
    TEST_ASSERT(mmffMolProperties->isValid());
    ForceFields::ForceField *field =
        RDKit::MMFF::constructForceField(*mol, mmffMolProperties);
    TEST_ASSERT(field);
    field->initialize();
    auto *dc = new ForceFields::MMFF::DistanceConstraintContrib(
        field, 1, 3, 2.0, 2.0, 1.0e5);
    field->contribs().push_back(ForceFields::ContribPtr(dc));
    field->minimize();
    TEST_ASSERT(MolTransforms::getBondLength(mol->getConformer(), 1, 3) > 1.99);

    auto *cfield = new ForceFields::ForceField(*field);
    cfield->positions().clear();

    for (unsigned int i = 0; i < cmol->getNumAtoms(); i++) {
      cfield->positions().push_back(&cmol->getConformer().getAtomPos(i));
    }
    cfield->initialize();
    cfield->minimize();
    TEST_ASSERT(MolTransforms::getBondLength(cmol->getConformer(), 1, 3) >
                1.99);
    TEST_ASSERT(RDKit::feq(field->calcEnergy(), cfield->calcEnergy()));

    const RDKit::Conformer &conf = mol->getConformer();
    const RDKit::Conformer &cconf = cmol->getConformer();
    for (unsigned int i = 0; i < mol->getNumAtoms(); i++) {
      RDGeom::Point3D p = conf.getAtomPos(i);
      RDGeom::Point3D cp = cconf.getAtomPos(i);
      TEST_ASSERT(RDKit::feq(p.x, cp.x));
      TEST_ASSERT(RDKit::feq(p.y, cp.y));
      TEST_ASSERT(RDKit::feq(p.z, cp.z));
    }

    delete field;
    delete cfield;
    delete mmffMolProperties;
    delete mol;
    delete cmol;
  }
  std::cerr << "  done" << std::endl;
}

int main(int argc, char *argv[]) {
  if (!mmffValidationSuite(argc, argv)) {
    testMMFFAllConstraints();
    testMMFFTorsionConstraints();
    testMMFFCopy();
  }

  return 0;
}
