//
// Copyright (C) David Cosgrove 2024.
//
//   @@ 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 <algorithm>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <list>
#include <memory>
#include <ranges>
#include <regex>
#include <sstream>
#include <thread>
#include <vector>

#include <boost/dynamic_bitset.hpp>
#include <boost/functional/hash.hpp>

#include <GraphMol/Chirality.h>
#include <GraphMol/MolOps.h>
#include <GraphMol/QueryAtom.h>
#include <GraphMol/ChemTransforms/ChemTransforms.h>
#include <GraphMol/SmilesParse/SmartsWrite.h>
#include <GraphMol/SynthonSpaceSearch/SynthonSpaceHitSet.h>
#include <GraphMol/SynthonSpaceSearch/SynthonSpace.h>
#include <GraphMol/SynthonSpaceSearch/SynthonSpaceSearch_details.h>
#include <GraphMol/SynthonSpaceSearch/ProgressBar.h>
#include <RDGeneral/ControlCHandler.h>
#include <RDGeneral/RDThreads.h>
#include <SimDivPickers/LeaderPicker.h>
#include <boost/variant2/variant.hpp>

namespace RDKit::SynthonSpaceSearch::details {

bool checkTimeOut(const TimePoint *endTime) {
  if (endTime != nullptr && Clock::now() > *endTime) {
    BOOST_LOG(rdWarningLog) << "Timed out.\n";
    return true;
  }
  return false;
}

// get a vector of vectors of unsigned ints that are all combinations of
// M items chosen from N e.g. all combinations of 3 bonds from a
// molecule. A modified form of the code in the first answer from
// https://stackoverflow.com/questions/12991758/creating-all-possible-k-combinations-of-n-items-in-c
std::vector<std::vector<unsigned int>> combMFromN(const unsigned int m,
                                                  const unsigned int n) {
  std::string allN(m, 1);
  allN.resize(n, 0);
  std::vector<std::vector<unsigned int>> combs;
  do {
    combs.emplace_back();
    for (unsigned int i = 0; i < n; ++i) {
      if (allN[i]) {
        combs.back().push_back(i);
      }
    }
  } while (std::prev_permutation(allN.begin(), allN.end()));
  return combs;
}

std::vector<std::vector<unsigned int>> permMFromN(const unsigned int m,
                                                  const unsigned int n) {
  std::vector<std::vector<unsigned int>> perms;
  auto combs = combMFromN(m, n);
  for (auto &c : combs) {
    do {
      perms.push_back(c);
    } while (std::next_permutation(c.begin(), c.end()));
  }

  return perms;
}

// The fragmentation is valid if the 2 ends of each bond are in different
// fragments.  This assumes there are no ring-closing reactions in the
// library, which is probably ok.
bool checkConnectorsInDifferentFrags(
    const std::vector<std::unique_ptr<ROMol>> &molFrags, const int numSplits) {
  // Loop over the isotope numbers of the ends of the splits
  for (const auto &frag : molFrags) {
    for (int j = 1; j <= numSplits; ++j) {
      int dummyCount = 0;
      for (const auto &atom : frag->atoms()) {
        if (!atom->getAtomicNum() &&
            atom->getIsotope() == static_cast<unsigned int>(j)) {
          if (dummyCount) {
            return false;
          }
          ++dummyCount;
        }
      }
    }
  }
  return true;
}

bool checkConnectorsInDifferentFrags(const ROMol &mol,
                                     const VECT_INT_VECT &fragIdxs,
                                     const int numSplits) {
  int dummyAtoms[2 * MAX_CONNECTOR_NUM + 2] = {};
  for (const auto &atom : mol.atoms()) {
    if (!atom->getAtomicNum()) {
      if (const auto dummy = atom->getIsotope(); dummy <= MAX_CONNECTOR_NUM) {
        if (const int pos = 2 * dummy; dummyAtoms[pos]) {
          dummyAtoms[pos + 1] = atom->getIdx() + 1;
        } else {
          dummyAtoms[pos] = atom->getIdx() + 1;
        }
      }
    }
  }
  for (const auto &fragIdx : fragIdxs) {
    for (int j = 0; j < numSplits; ++j) {
      if (dummyAtoms[2 * j]) {
        const int d1 = dummyAtoms[2 * j] - 1;
        const int d2 = dummyAtoms[2 * j + 1] - 1;
        int dummyCount = 0;
        for (const auto fi : fragIdx) {
          if (fi == d1 || fi == d2) {
            if (dummyCount) {
              return false;
            }
            ++dummyCount;
          }
        }
      }
    }
  }
  return true;
}

// Traverse the bonds from aromBond and return all the ones that are aromatic.
std::vector<const Bond *> getContiguousAromaticBonds(const ROMol &mol,
                                                     const Bond *aromBond) {
  std::vector<const Bond *> aromBonds(1, aromBond);
  std::list<const Bond *> toDo(1, aromBond);
  boost::dynamic_bitset<> done(mol.getNumBonds());
  done[aromBond->getIdx()] = true;
  while (!toDo.empty()) {
    const auto nextBond = toDo.front();
    toDo.pop_front();
    for (const auto nbr :
         make_iterator_range(mol.getAtomNeighbors(nextBond->getBeginAtom()))) {
      if (auto bond = mol.getBondBetweenAtoms(nextBond->getBeginAtomIdx(), nbr);
          !done[bond->getIdx()] && bond->getIsAromatic()) {
        aromBonds.push_back(bond);
        done[bond->getIdx()] = true;
        toDo.push_back(bond);
      }
    }
    for (const auto nbr :
         make_iterator_range(mol.getAtomNeighbors(nextBond->getEndAtom()))) {
      if (auto bond = mol.getBondBetweenAtoms(nextBond->getEndAtomIdx(), nbr);
          !done[bond->getIdx()] && bond->getIsAromatic()) {
        aromBonds.push_back(bond);
        done[bond->getIdx()] = true;
        toDo.push_back(bond);
      }
    }
  }
  return aromBonds;
}

namespace {
boost::dynamic_bitset<> flagRingBonds(const ROMol &mol) {
  const auto ringInfo = mol.getRingInfo();
  if (!ringInfo->isInitialized()) {
    // Query molecules don't seem to have the ring info generated on creation.
    MolOps::findSSSR(mol);
  }
  boost::dynamic_bitset<> ringBonds(mol.getNumBonds());
  for (const auto &r : ringInfo->bondRings()) {
    for (const auto b : r) {
      ringBonds.set(b);
    }
  }
  return ringBonds;
}

void addBondsToList(const ROMol &mol, const Atom *atom,
                    boost::dynamic_bitset<> &ringBonds,
                    boost::dynamic_bitset<> &doneAtoms,
                    std::list<const Atom *> &atoms,
                    std::vector<const Bond *> &ringBlock) {
  for (const auto nbond : mol.atomBonds(atom)) {
    if (ringBonds[nbond->getIdx()]) {
      ringBonds.set(nbond->getIdx(), false);
      ringBlock.push_back(nbond);
      if (const Atom *otherAtom = nbond->getOtherAtom(atom);
          !doneAtoms[otherAtom->getIdx()]) {
        atoms.push_back(otherAtom);
        doneAtoms.set(otherAtom->getIdx(), true);
      }
    }
  }
}

// Get all the contiguous ring blocks, e.g. in c1ccccc1Oc1cccc2[nH]ccc12
// get the benzene and indole as separate pieces.  Pass ringBonds by
// value as it gets changed and we'll be needing it again later.
std::vector<std::vector<const Bond *>> getRingBlocks(
    const ROMol &mol, boost::dynamic_bitset<> ringBonds) {
  std::vector<std::vector<const Bond *>> ringBlocks;
  while (ringBonds.count()) {
    for (const auto bond : mol.bonds()) {
      if (ringBonds[bond->getIdx()]) {
        ringBlocks.emplace_back(std::vector<const Bond *>{bond});
        ringBonds.set(bond->getIdx(), false);
        boost::dynamic_bitset<> doneAtoms(mol.getNumAtoms());
        std::list<const Atom *> toDo;
        addBondsToList(mol, bond->getBeginAtom(), ringBonds, doneAtoms, toDo,
                       ringBlocks.back());
        addBondsToList(mol, bond->getEndAtom(), ringBonds, doneAtoms, toDo,
                       ringBlocks.back());
        while (!toDo.empty()) {
          const auto nextAtom = toDo.front();
          toDo.pop_front();
          addBondsToList(mol, nextAtom, ringBonds, doneAtoms, toDo,
                         ringBlocks.back());
        }
        std::sort(ringBlocks.back().begin(), ringBlocks.back().end(),
                  [](const Bond *b1, const Bond *b2) -> bool {
                    return b1->getIdx() < b2->getIdx();
                  });
      }
    }
  }
  return ringBlocks;
}

bool bondPairFragmentsBlock(
    const size_t bondi, const size_t bondj, const unsigned int numAtoms,
    const std::vector<const Bond *> &ringBlock,
    std::vector<boost::dynamic_bitset<>> &ringAdjTable) {
  const Bond *bi = ringBlock[bondi];
  const Bond *bj = ringBlock[bondj];

  // Temporarily break the 2 bonds
  ringAdjTable[bi->getBeginAtomIdx()][bi->getEndAtomIdx()] = false;
  ringAdjTable[bi->getEndAtomIdx()][bi->getBeginAtomIdx()] = false;
  ringAdjTable[bj->getBeginAtomIdx()][bj->getEndAtomIdx()] = false;
  ringAdjTable[bj->getEndAtomIdx()][bj->getBeginAtomIdx()] = false;

  std::list<size_t> atoms(1, ringBlock[bondi]->getBeginAtomIdx());
  std::list<size_t> toDo(1, ringBlock[bondi]->getBeginAtomIdx());
  boost::dynamic_bitset<> doneAtom(ringAdjTable.size());
  doneAtom[ringBlock[bondi]->getBeginAtomIdx()] = true;
  while (!toDo.empty()) {
    const auto nextAtom = toDo.front();
    toDo.pop_front();
    const auto &theseConns = ringAdjTable[nextAtom];
    for (size_t i = 0; i < ringAdjTable.size(); i++) {
      if (theseConns[i] && !doneAtom[i]) {
        doneAtom[i] = true;
        toDo.push_back(i);
        atoms.push_back(i);
      }
    }
  }

  ringAdjTable[bi->getBeginAtomIdx()][bi->getEndAtomIdx()] = true;
  ringAdjTable[bi->getEndAtomIdx()][bi->getBeginAtomIdx()] = true;
  ringAdjTable[bj->getBeginAtomIdx()][bj->getEndAtomIdx()] = true;
  ringAdjTable[bj->getEndAtomIdx()][bj->getBeginAtomIdx()] = true;

  return atoms.size() < numAtoms;
}

void makeRingAtomAdjTable(const ROMol &mol,
                          const boost::dynamic_bitset<> &ringBonds,
                          std::vector<boost::dynamic_bitset<>> &ringAdjTable) {
  ringAdjTable = std::vector<boost::dynamic_bitset<>>(
      mol.getNumAtoms(), boost::dynamic_bitset<>(mol.getNumAtoms()));
  for (const auto bond : mol.bonds()) {
    if (ringBonds[bond->getIdx()]) {
      ringAdjTable[bond->getBeginAtomIdx()][bond->getEndAtomIdx()] = true;
      ringAdjTable[bond->getEndAtomIdx()][bond->getBeginAtomIdx()] = true;
    }
  }
}

// Take out any pairs of ring bonds that don't fragment the molecule.  These
// will be in fused ring systems where one of the bonds is in 1 sub-ring,
// one is in the other.
void findBondPairsThatFragment(
    const ROMol &mol, const boost::dynamic_bitset<> &ringBonds,
    const std::vector<std::vector<const Bond *>> &ringBlocks,
    std::vector<std::pair<unsigned int, unsigned int>> &ringBondPairs) {
  std::vector<boost::dynamic_bitset<>> ringAdjTable;
  makeRingAtomAdjTable(mol, ringBonds, ringAdjTable);
  // If all the atoms in the bond are 2 connected, it's a simple ring so
  // nothing to do.
  for (const auto &ringBlock : ringBlocks) {
    bool ok = true;
    boost::dynamic_bitset<> blockAtoms(mol.getNumAtoms());
    for (const auto bond : ringBlock) {
      blockAtoms[bond->getBeginAtomIdx()] = true;
      blockAtoms[bond->getEndAtomIdx()] = true;
      if (ringAdjTable[bond->getBeginAtomIdx()].count() > 2 ||
          ringAdjTable[bond->getEndAtomIdx()].count() > 2) {
        ok = false;
      }
    }
    if (ok) {
      for (size_t i = 0; i < ringBlock.size() - 1; ++i) {
        for (size_t j = i + 1; j < ringBlock.size(); ++j) {
          ringBondPairs.emplace_back(
              std::make_pair(ringBlock[i]->getIdx(), ringBlock[j]->getIdx()));
        }
      }
    } else {
      // Need to check if each pair makes 2 fragments before adding.
      const unsigned int numAtoms = blockAtoms.count();
      for (size_t i = 0; i < ringBlock.size() - 1; ++i) {
        for (size_t j = i + 1; j < ringBlock.size(); ++j) {
          if (bondPairFragmentsBlock(i, j, numAtoms, ringBlock, ringAdjTable)) {
            ringBondPairs.emplace_back(
                std::make_pair(ringBlock[i]->getIdx(), ringBlock[j]->getIdx()));
          }
        }
      }
    }
  }
}

void makeFragmentsForMol(
    const ROMol &mol, const std::vector<std::vector<unsigned int>> &splitBonds,
    size_t splitBondNum,
    const std::vector<std::pair<unsigned int, unsigned int>> &dummyLabels,
    const unsigned int maxNumFrags, const boost::dynamic_bitset<> &ringBonds,
    std::vector<std::pair<std::string, std::shared_ptr<ROMol>>> &fragments) {
  // first, see how many fragments we're going to get. The ring bonds
  // are paired so they will split the same ring.
  int numRingBonds = 0;
  int numNonRingBonds = 0;
  for (const auto sb : splitBonds[splitBondNum]) {
    if (ringBonds[sb]) {
      numRingBonds++;
    } else {
      numNonRingBonds++;
    }
  }
  if (const unsigned int numFragsPoss = 1 + numNonRingBonds + numRingBonds / 2;
      numFragsPoss > maxNumFrags) {
    return;
  }
  auto fragMol = MolFragmenter::fragmentOnBonds(mol, splitBonds[splitBondNum],
                                                true, &dummyLabels);
  const std::string fragSmi(MolToSmiles(*fragMol));
  fragments[splitBondNum] =
      std::pair<std::string, std::shared_ptr<ROMol>>(fragSmi, fragMol);
}

void doPartInitialFragmentation(
    const ROMol &mol, const std::vector<std::vector<unsigned int>> &splitBonds,
    const unsigned int maxNumFrags, const boost::dynamic_bitset<> &ringBonds,
    const TimePoint *endTime, std::atomic<std::int64_t> &mostRecentRingBond,
    std::int64_t lastRingBond,
    const std::vector<std::pair<unsigned int, unsigned int>> &dummyLabels,
    std::vector<std::pair<std::string, std::shared_ptr<ROMol>>> &tmpFrags) {
  int numTries = 100;
  bool timedOut = false;
  while (true) {
    std::int64_t thisRB = ++mostRecentRingBond;
    if (thisRB > lastRingBond) {
      break;
    }
    makeFragmentsForMol(mol, splitBonds, thisRB, dummyLabels, maxNumFrags,
                        ringBonds, tmpFrags);
    --numTries;
    if (!numTries) {
      numTries = 100;
      timedOut = checkTimeOut(endTime);
      if (timedOut) {
        break;
      }
    }
  }
}

void doInitialFragmentation(
    const ROMol &mol, const std::vector<std::vector<unsigned int>> &splitBonds,
    const unsigned int maxNumFrags, const boost::dynamic_bitset<> &ringBonds,
    [[maybe_unused]] const int numThreads, const TimePoint *endTime,
    bool &timedOut,
    std::vector<std::pair<std::string, std::shared_ptr<ROMol>>> &tmpFrags) {
  std::vector<std::pair<unsigned int, unsigned int>> dummyLabels;
  for (unsigned int i = 1; i <= MAX_CONNECTOR_NUM; ++i) {
    dummyLabels.emplace_back(i, i);
  }

  // Now do the splits.  Symmetrical molecules can give rise to the same
  // fragment set in different ways so keep track of what we've had to
  // avoid duplicates.
  std::int64_t lastRingBond = splitBonds.size() - 1;
  std::atomic<std::int64_t> mostRecentRingBond = -1;
  if (const auto numThreadsToUse = getNumThreadsToUse(numThreads);
      numThreads > 1) {
    std::vector<std::thread> threads;
    for (unsigned int i = 0U;
         i <
         std::min(static_cast<std::int64_t>(numThreadsToUse), lastRingBond + 1);
         ++i) {
      threads.push_back(std::thread(doPartInitialFragmentation, std::ref(mol),
                                    std::ref(splitBonds), maxNumFrags,
                                    std::ref(ringBonds), endTime,
                                    std::ref(mostRecentRingBond), lastRingBond,
                                    std::ref(dummyLabels), std::ref(tmpFrags)));
    }
    for (auto &t : threads) {
      t.join();
    }
  } else {
    doPartInitialFragmentation(mol, splitBonds, maxNumFrags, ringBonds, endTime,
                               std::ref(mostRecentRingBond), lastRingBond,
                               dummyLabels, tmpFrags);
  }
  timedOut = details::checkTimeOut(endTime);
}

void doPartFinalFragmentation(
    const std::vector<std::pair<std::string, std::shared_ptr<ROMol>>> &tmpFrags,
    unsigned int maxNumFrags, const TimePoint *endTime,
    std::atomic<std::int64_t> &mostRecentFrag, std::int64_t lastFrag,
    std::vector<std::vector<std::shared_ptr<ROMol>>> &fragments) {
  int numTries = 100;
  bool timedOut = false;
  while (true) {
    std::int64_t thisFrag = ++mostRecentFrag;
    if (thisFrag > lastFrag) {
      break;
    }
    if (std::vector<std::unique_ptr<ROMol>> molFrags;
        MolOps::getMolFrags(*tmpFrags[thisFrag].second, molFrags, false) <=
        maxNumFrags) {
      // The first fragment was made from the whole query and is already in
      // fragments.
      fragments[thisFrag + 1].resize(molFrags.size());
      for (unsigned int i = 0; i < molFrags.size(); ++i) {
        fragments[thisFrag + 1][i].reset(molFrags[i].release());
      }
    }
    --numTries;
    if (!numTries) {
      numTries = 100;
      timedOut = checkTimeOut(endTime);
      if (timedOut) {
        break;
      }
    }
  }
}

void doFinalFragmentation(
    const std::vector<std::pair<std::string, std::shared_ptr<ROMol>>> &tmpFrags,
    unsigned int maxNumFrags, [[maybe_unused]] int numThreads,
    const TimePoint *endTime, bool &timedOut,
    std::vector<std::vector<std::shared_ptr<ROMol>>> &fragments) {
  std::int64_t lastFrag = tmpFrags.size() - 1;
  std::atomic<std::int64_t> mostRecentFrag = -1;
  if (const auto numThreadsToUse = getNumThreadsToUse(numThreads);
      numThreads > 1) {
    std::vector<std::thread> threads;
    for (unsigned int i = 0U;
         i < std::min(static_cast<std::int64_t>(numThreadsToUse), lastFrag + 1);
         ++i) {
      threads.push_back(std::thread(
          doPartFinalFragmentation, std::ref(tmpFrags), maxNumFrags, endTime,
          std::ref(mostRecentFrag), lastFrag, std::ref(fragments)));
    }
    for (auto &t : threads) {
      t.join();
    }
  } else {
    doPartFinalFragmentation(tmpFrags, maxNumFrags, endTime, mostRecentFrag,
                             lastFrag, fragments);
  }
  timedOut = details::checkTimeOut(endTime);
}

// Recursively build all combinations of maxBondSplits sets of
// bondPairs into splitBonds, removing any duplicate bonds.
void buildSplitBonds(
    const std::vector<std::pair<unsigned int, unsigned int>> &bondPairs,
    const unsigned int maxBondSplits,
    std::vector<std::vector<unsigned int>> &splitBonds) {
  if (bondPairs.size() == 1) {
    splitBonds.push_back({bondPairs[0].first});
    return;
  }
  std::vector<unsigned int> nextSplits;
  splitBonds.reserve(maxBondSplits * maxBondSplits * bondPairs.size());
  for (unsigned int i = 1; i < maxBondSplits; ++i) {
    auto combs = combMFromN(i, static_cast<int>(bondPairs.size()));
    for (const auto &comb : combs) {
      nextSplits.clear();
      for (const auto c : comb) {
        nextSplits.push_back(bondPairs[c].first);
        nextSplits.push_back(bondPairs[c].second);
      }
      std::sort(nextSplits.begin(), nextSplits.end());
      nextSplits.erase(std::unique(nextSplits.begin(), nextSplits.end()),
                       nextSplits.end());
      // Each split will need a connector num, so any split set that will
      // produce one higher than the SynthonSpace has been set up for is
      // a bust.  Splitting 3 rings each once will produce 4 fragments
      // and 6 broken bonds, for example.
      if (nextSplits.size() > MAX_CONNECTOR_NUM) {
        continue;
      }
      nextSplits.shrink_to_fit();
      splitBonds.push_back(nextSplits);
    }
  }
  std::sort(splitBonds.begin(), splitBonds.end());
  splitBonds.erase(std::unique(splitBonds.begin(), splitBonds.end()),
                   splitBonds.end());
}

void uniquifyFragsBySmiles(
    std::vector<std::pair<std::string, std::shared_ptr<ROMol>>> &tmpFrags,
    const std::atomic_uint64_t maxNumFragSets) {
  // Keep unique SMILES only
  std::sort(tmpFrags.begin(), tmpFrags.end(),
            [](const auto &lhs, const auto &rhs) -> bool {
              return lhs.first < rhs.first;
            });
  tmpFrags.erase(std::unique(tmpFrags.begin(), tmpFrags.end(),
                             [](const auto &lhs, const auto &rhs) -> bool {
                               return lhs.first == rhs.first;
                             }),
                 tmpFrags.end());
  if (tmpFrags.size() > maxNumFragSets) {
    tmpFrags.erase(tmpFrags.begin() + maxNumFragSets, tmpFrags.end());
  }
}

void uniquifyFragsByAtoms(
    std::vector<std::pair<std::string, std::shared_ptr<ROMol>>> &tmpFrags,
    const std::uint64_t maxNumFragSets) {
  std::vector<std::pair<std::string, size_t>> fragsToKeep;
  fragsToKeep.reserve(tmpFrags.size());
  for (size_t i = 0; i < tmpFrags.size(); ++i) {
    std::vector<std::vector<int>> molFrags;
    MolOps::getMolFrags(*tmpFrags[i].second, molFrags);
    std::vector<std::string> atStrings;
    atStrings.reserve(molFrags.size());
    for (auto &mf : molFrags) {
      std::ranges::sort(mf);
      atStrings.push_back(std::to_string(mf[0]));
      for (unsigned int j = 1; j < mf.size(); ++j) {
        atStrings.back() += "_" + std::to_string(mf[j]);
      }
    }
    std::ranges::sort(atStrings);
    std::string atString = atStrings.front();
    for (unsigned int j = 1; j < atStrings.size(); ++j) {
      atString += "." + atStrings[j];
    }
    fragsToKeep.emplace_back(atString, i);
  }
  std::sort(fragsToKeep.begin(), fragsToKeep.end(),
            [](const auto &lhs, const auto &rhs) -> bool {
              return lhs.first < rhs.first;
            });
  fragsToKeep.erase(std::unique(fragsToKeep.begin(), fragsToKeep.end(),
                                [](const auto &lhs, const auto &rhs) -> bool {
                                  return lhs.first == rhs.first;
                                }),
                    fragsToKeep.end());
  std::vector<std::pair<std::string, std::shared_ptr<ROMol>>> newTmpFrags(
      fragsToKeep.size());
  for (size_t i = 0;
       i <
       std::min(static_cast<std::uint64_t>(fragsToKeep.size()), maxNumFragSets);
       ++i) {
    newTmpFrags[i] = std::move(tmpFrags[fragsToKeep[i].second]);
  }
  tmpFrags = std::move(newTmpFrags);
}
}  // namespace

std::vector<std::vector<std::shared_ptr<ROMol>>> splitMolecule(
    const ROMol &query, unsigned int maxNumFrags,
    const std::uint64_t maxNumFragSets, const TimePoint *endTime,
    const int numThreads, const FragSetUniquifyMode &uniquifyMode,
    bool &timedOut) {
  if (maxNumFrags < 1) {
    maxNumFrags = 1;
  }
  maxNumFrags = std::min(maxNumFrags, query.getNumBonds() + 1);

  auto ringBonds = flagRingBonds(query);

  // Now get all contiguous ring blocks
  const auto ringBlocks = getRingBlocks(query, ringBonds);

  // Collect all the bond pairs that can fragment the molecule.
  std::vector<std::pair<unsigned int, unsigned int>> bondPairs;
  findBondPairsThatFragment(query, ringBonds, ringBlocks, bondPairs);
  // And all the non-ring bonds, which clearly can all make 2 fragments
  // when broken.  Put them in as pairs of the same value, for ease of
  // processing below.  We also aren't interested in H atoms as a fragment.
  for (const auto b : query.bonds()) {
    if (!ringBonds[b->getIdx()] && b->getBeginAtom()->getAtomicNum() != 1 &&
        b->getEndAtom()->getAtomicNum() != 1) {
      bondPairs.push_back({b->getIdx(), b->getIdx()});
    }
  }

  std::vector<std::vector<unsigned int>> splitBonds;
  buildSplitBonds(bondPairs, maxNumFrags, splitBonds);
  std::vector<std::pair<std::string, std::shared_ptr<ROMol>>> tmpFrags(
      splitBonds.size());

  // First split leaves the fragments in the same molecule, and returns
  // the SMILES for it.
  doInitialFragmentation(query, splitBonds, maxNumFrags, ringBonds, numThreads,
                         endTime, timedOut, tmpFrags);
  std::vector<std::vector<std::shared_ptr<ROMol>>> fragments;
  if (timedOut || ControlCHandler::getGotSignal()) {
    return fragments;
  }
  switch (uniquifyMode) {
    case FragSetUniquifyMode::BySmiles:
      uniquifyFragsBySmiles(tmpFrags, maxNumFragSets);
      break;
    case FragSetUniquifyMode::ByAtoms:
      uniquifyFragsByAtoms(tmpFrags, maxNumFragSets);
      break;
  }

  // Keep the molecule itself (i.e. 0 splits).  It will probably produce
  // lots of hits but it is necessary if, for example, the query is a match
  // for a single synthon set.
  fragments.resize(tmpFrags.size() + 1);
  fragments.emplace_back();
  fragments.back().emplace_back(new ROMol(query));
  // And now split the molecules into the final fragments.
  doFinalFragmentation(tmpFrags, maxNumFrags, numThreads, endTime, timedOut,
                       fragments);

  fragments.erase(
      std::remove_if(fragments.begin(), fragments.end(),
                     [](const auto &fs) -> bool { return fs.empty(); }),
      fragments.end());
  return fragments;
}

int countConnections(const ROMol &mol) {
  int res = 0;
  for (const auto atom : mol.atoms()) {
    if (!atom->getAtomicNum() && atom->getIsotope() >= 1 &&
        atom->getIsotope() <= MAX_CONNECTOR_NUM) {
      ++res;
    }
  }
  return res;
}

std::vector<boost::dynamic_bitset<>> getConnectorPatterns(
    const std::vector<std::shared_ptr<ROMol>> &mols) {
  std::vector<boost::dynamic_bitset<>> connPatterns(
      mols.size(), boost::dynamic_bitset<>(MAX_CONNECTOR_NUM + 1));
  for (size_t i = 0; i < mols.size(); i++) {
    for (const auto &a : mols[i]->atoms()) {
      if (!a->getAtomicNum() && a->getIsotope() &&
          a->getIsotope() <= MAX_CONNECTOR_NUM) {
        connPatterns[i].set(a->getIsotope());
      }
    }
  }
  return connPatterns;
}

boost::dynamic_bitset<> getConnectorPattern(
    const std::vector<std::shared_ptr<ROMol>> &fragSet) {
  boost::dynamic_bitset<> conns(MAX_CONNECTOR_NUM + 1);
  const auto connPatterns = getConnectorPatterns(fragSet);
  for (const auto &cp : connPatterns) {
    conns |= cp;
  }
  return conns;
}

namespace {
std::vector<int> bitsToInts(const boost::dynamic_bitset<> &bits) {
  std::vector<int> ints;
  for (size_t i = 0; i < bits.size(); ++i) {
    if (bits[i]) {
      ints.push_back(static_cast<int>(i));
    }
  }
  return ints;
}
}  // namespace

std::vector<std::vector<std::vector<std::pair<Atom *, unsigned int>>>>
getConnectorPermutations(const std::vector<std::unique_ptr<ROMol>> &molFrags,
                         const boost::dynamic_bitset<> &fragConns,
                         const boost::dynamic_bitset<> &reactionConns) {
  const auto numFragConns = fragConns.count();
  auto rConns = bitsToInts(reactionConns);
  const auto perms = permMFromN(numFragConns, reactionConns.count());

  std::vector<std::vector<std::vector<std::pair<Atom *, unsigned int>>>>
      fragConnPerms;
  fragConnPerms.reserve(perms.size());

  for (const auto &perm : perms) {
    fragConnPerms.emplace_back();
    // Copy the fragments and set the isotope numbers according to this
    // permutation.
    for (const auto &f : molFrags) {
      fragConnPerms.back().emplace_back();
      boost::dynamic_bitset<> atomDone(f->getNumAtoms());
      for (const auto atom : f->atoms()) {
        if (!atom->getAtomicNum()) {
          for (size_t i = 0; i < perm.size(); ++i) {
            if (!atomDone[atom->getIdx()] && atom->getIsotope() == i + 1) {
              fragConnPerms.back().back().emplace_back(atom, perm[i] + 1);
              atomDone[atom->getIdx()] = true;
            }
          }
        }
      }
    }
  }

  return fragConnPerms;
}

std::vector<std::vector<boost::dynamic_bitset<>>> getConnectorPermutations(
    const std::vector<boost::dynamic_bitset<>> &fragConnPatts,
    const boost::dynamic_bitset<> &reactionConns) {
  boost::dynamic_bitset<> conns(MAX_CONNECTOR_NUM + 1);
  for (auto &fragConnPatt : fragConnPatts) {
    conns |= fragConnPatt;
  }

  const auto numFragConns = conns.count();
  auto rConns = bitsToInts(reactionConns);
  const auto perms = permMFromN(numFragConns, reactionConns.count());
  std::vector<std::vector<boost::dynamic_bitset<>>> retBitsets;
  for (const auto &perm : perms) {
    retBitsets.emplace_back();
    for (const auto &fragConnPatt : fragConnPatts) {
      boost::dynamic_bitset<> bs(MAX_CONNECTOR_NUM + 1);
      for (size_t i = 0; i < perm.size(); ++i) {
        if (fragConnPatt[i + 1]) {
          bs.set(perm[i] + 1);
        }
      }
      retBitsets.back().push_back(bs);
    }
  }
  return retBitsets;
}

void expandBitSet(std::vector<boost::dynamic_bitset<>> &bitSets) {
  const bool someSet = std::any_of(
      bitSets.begin(), bitSets.end(),
      [](const boost::dynamic_bitset<> &bs) -> bool { return bs.any(); });
  if (someSet) {
    for (auto &bs : bitSets) {
      if (!bs.count()) {
        bs.set();
      }
    }
  }
}

void bitSetsToVectors(const std::vector<boost::dynamic_bitset<>> &bitSets,
                      std::vector<std::vector<size_t>> &outVecs) {
  outVecs.resize(bitSets.size());
  for (size_t i = 0; i < bitSets.size(); ++i) {
    outVecs[i].reserve(bitSets[i].count());
    for (size_t j = 0; j < bitSets[i].size(); j++) {
      if (bitSets[i][j]) {
        outVecs[i].push_back(j);
      }
    }
  }
}

bool removeQueryAtoms(RWMol &mol) {
  bool didSomething = false;
  for (const Atom *atom : mol.atoms()) {
    if ((atom->getAtomicNum() || !atom->getIsotope()) && atom->hasQuery() &&
        atom->getQuery()->getDescription() != "AtomType") {
      std::unique_ptr<QueryAtom> qat;
      if (atom->getAtomicNum()) {
        qat.reset(new QueryAtom(atom->getAtomicNum()));
      } else {
        qat.reset(new QueryAtom());
        qat->setQuery(makeAAtomQuery());
      }
      mol.replaceAtom(atom->getIdx(), qat.get());
      didSomething = true;
    }
  }
  return didSomething;
}

std::unique_ptr<ROMol> buildConnRegion(const ROMol &mol) {
  boost::dynamic_bitset<> inFrag(mol.getNumAtoms());
  for (const auto a : mol.atoms()) {
    if (!a->getAtomicNum() && a->getIsotope()) {
      inFrag[a->getIdx()] = true;
      for (const auto &n1 : mol.atomNeighbors(a)) {
        inFrag[n1->getIdx()] = true;
        for (const auto &n2 : mol.atomNeighbors(n1)) {
          inFrag[n2->getIdx()] = true;
          for (const auto &n3 : mol.atomNeighbors(n2)) {
            inFrag[n3->getIdx()] = true;
          }
        }
      }
    }
  }
  if (!inFrag.count()) {
    return std::unique_ptr<RWMol>();
  }

  std::unique_ptr<RWMol> molCp(new RWMol(mol));
  molCp->beginBatchEdit();
  for (const auto aCp : molCp->atoms()) {
    if (!inFrag[aCp->getIdx()]) {
      molCp->removeAtom(aCp);
    } else {
      if (!aCp->getAtomicNum()) {
        if (aCp->getIsotope()) {
          aCp->setIsotope(1);
          if (aCp->hasQuery()) {
            aCp->expandQuery(makeAtomIsotopeQuery(1), Queries::COMPOSITE_OR);
          }
        }
      }
    }
  }
  molCp->commitBatchEdit();
  return molCp;
}

std::string buildProductName(const std::string &reactionId,
                             const std::vector<std::string> &fragIds) {
  std::string prodName = "";
  for (const auto &fragId : fragIds) {
    if (prodName != "") {
      prodName += ";";
    }
    prodName += fragId;
  }
  prodName += ";" + reactionId;
  return prodName;
}

std::string buildProductName(const std::string &reactionId,
                             const std::vector<const std::string *> &fragIds) {
  std::string prodName = "";
  for (const auto &fragId : fragIds) {
    if (prodName != "") {
      prodName += ";";
    }
    prodName += *fragId;
  }
  prodName += ";" + reactionId;
  return prodName;
}

std::string buildProductName(
    const RDKit::SynthonSpaceSearch::SynthonSpaceHitSet *hitset,
    const std::vector<size_t> &fragNums) {
  std::string prodName = "";
  for (size_t i = 0; i < fragNums.size(); ++i) {
    if (prodName != "") {
      prodName += ";";
    }
    prodName += hitset->synthonsToUse[i][fragNums[i]].first;
  }
  prodName += ";" + hitset->d_reaction->getId();
  return prodName;
}

std::size_t buildProductHash(
    const RDKit::SynthonSpaceSearch::SynthonSpaceHitSet *hitset,
    const std::vector<size_t> &fragNums) {
  std::size_t seed = 0;
  for (size_t i = 0; i < fragNums.size(); ++i) {
    boost::hash_combine(seed, hitset->synthonsToUse[i][fragNums[i]].first);
  }
  boost::hash_combine(seed, hitset->d_reaction->getId());
  return seed;
}

std::unique_ptr<ROMol> buildProduct(
    const std::vector<const ROMol *> &synthons) {
  MolzipParams mzparams;
  mzparams.label = MolzipLabel::Isotope;
  mzparams.alignCoordinates = true;
  auto prodMol = std::make_unique<RWMol>(*synthons.front());
  for (size_t i = 1; i < synthons.size(); ++i) {
    prodMol->insertMol(*synthons[i]);
  }
  std::unique_ptr<ROMol> zipProdMol;
  try {
    zipProdMol = molzip(*prodMol, mzparams);
    MolOps::sanitizeMol(*dynamic_cast<RWMol *>(zipProdMol.get()));
  } catch (std::exception &e) {
    std::cout << "AWOOGA :: Failed to zip " << MolToSmiles(*prodMol)
              << " because " << std::endl
              << e.what() << std::endl;
  }
  return zipProdMol;
}

std::map<std::string, std::vector<ROMol *>> mapFragsBySmiles(
    std::vector<std::vector<std::shared_ptr<ROMol>>> &fragSets,
    bool &cancelled) {
  std::map<std::string, std::vector<ROMol *>> fragSmiToFrag;
  for (auto &fragSet : fragSets) {
    for (auto &frag : fragSet) {
      if (ControlCHandler::getGotSignal()) {
        cancelled = true;
        return fragSmiToFrag;
      }
      // For the fingerprints and shapes, ring info is required.
      unsigned int otf;
      sanitizeMol(*static_cast<RWMol *>(frag.get()), otf,
                  MolOps::SANITIZE_SYMMRINGS);
      std::string fragSmi = MolToSmiles(*frag);
      if (auto it = fragSmiToFrag.find(fragSmi); it == fragSmiToFrag.end()) {
        fragSmiToFrag.emplace(fragSmi, std::vector<ROMol *>(1, frag.get()));
      } else {
        it->second.emplace_back(frag.get());
      }
    }
  }
  return fragSmiToFrag;
}

unsigned int countChiralAtoms(ROMol &mol, unsigned int *numExcDummies) {
  auto sis = Chirality::findPotentialStereo(mol, true, true);
  unsigned int numChiralAtoms = 0;
  if (numExcDummies) {
    *numExcDummies = 0;
  }
  for (auto &si : sis) {
    if (si.type != Chirality::StereoType::Atom_Tetrahedral) {
      continue;
    }
    ++numChiralAtoms;
    if (numExcDummies) {
      auto atom = mol.getAtomWithIdx(si.centeredOn);
      unsigned int numDummies = 0;
      for (auto nbr : mol.atomNeighbors(atom)) {
        if (nbr->getAtomicNum() == 0 &&
            nbr->getIsotope() <= MAX_CONNECTOR_NUM) {
          numDummies++;
        }
      }
      if (numDummies < 2) {
        (*numExcDummies)++;
      }
    }
  }
  return numChiralAtoms;
}

bool hasUnspecifiedStereo(ROMol &mol) {
  auto sis = Chirality::findPotentialStereo(mol, true, true);
  for (auto &si : sis) {
    if (si.type == Chirality::StereoType::Atom_Tetrahedral ||
        si.type == Chirality::StereoType::Atom_SquarePlanar ||
        si.type == Chirality::StereoType::Atom_TrigonalBipyramidal ||
        si.type == Chirality::StereoType::Atom_Octahedral) {
      Atom *atom = mol.getAtomWithIdx(si.centeredOn);
      if (atom->getChiralTag() == Atom::CHI_UNSPECIFIED) {
        return true;
      }
    } else if (si.type == Chirality::StereoType::Bond_Double ||
               si.type == Chirality::StereoType::Bond_Cumulene_Even ||
               si.type == Chirality::StereoType::Bond_Atropisomer) {
      Bond *bond = mol.getBondWithIdx(si.centeredOn);
      if (bond->getStereo() == Bond::BondStereo::STEREONONE ||
          bond->getStereo() == Bond::BondStereo::STEREOANY) {
        return true;
      }
    }
  }
  return false;
}

namespace {
void useUserConfGen(std::unique_ptr<RWMol> &mol, unsigned int numConformers,
                    UserConfGenerator &userConfGenerator) {
  auto newMol = userConfGenerator(MolToSmiles(*mol), numConformers);
  // The mol has information that won't have survived, and it's not safe to
  // assume the atom orders have survived.
  MatchVectType res;
  if (SubstructMatch(*newMol, *mol, res)) {
    std::map<unsigned int, unsigned int> query2Mol;
    std::ranges::copy(res, std::inserter(query2Mol, query2Mol.end()));
    for (const auto &p : res) {
      auto currAt = mol->getAtomWithIdx(p.first);
      auto newAt = newMol->getAtomWithIdx(p.second);
      if (currAt->hasProp("molNum")) {
        newAt->setProp<unsigned int>("molNum",
                                     currAt->getProp<unsigned int>("molNum"));
      }
      if (currAt->hasProp("idx")) {
        newAt->setProp<unsigned int>("idx",
                                     currAt->getProp<unsigned int>("idx"));
      }
      if (currAt->hasProp("HoldsJoin")) {
        newAt->setProp<std::string>("HoldsJoin",
                                    currAt->getProp<std::string>("HoldsJoin"));
      }
      for (const auto &nbr : mol->atomNeighbors(currAt)) {
        if (auto it = query2Mol.find(nbr->getIdx()); it != query2Mol.end()) {
          auto oldBond = mol->getBondBetweenAtoms(p.first, nbr->getIdx());
          auto newBond = newMol->getBondBetweenAtoms(p.second, it->second);
          if (oldBond->hasProp("molNum")) {
            newBond->setProp<unsigned int>(
                "molNum", oldBond->getProp<unsigned int>("molNum"));
          }
          if (oldBond->hasProp("idx")) {
            newBond->setProp<unsigned int>(
                "idx", oldBond->getProp<unsigned int>("idx"));
          }
          if (oldBond->hasProp("HoldsJoin")) {
            newBond->setProp<std::string>(
                "HoldsJoin", oldBond->getProp<std::string>("HoldsJoin"));
          }
        }
      }
    }
  } else {
    // Bit weird!
    BOOST_LOG(rdErrorLog) << "Couldn't generate conformers for isomer "
                          << MolToCXSmiles(*mol) << " of molecule."
                          << std::endl;
    mol.reset();
    return;
  }
  if (mol->hasProp(common_properties::_Name)) {
    newMol->setProp<std::string>(
        common_properties::_Name,
        mol->getProp<std::string>(common_properties::_Name));
  }
  mol.reset(newMol.release());
}

void useDefaultConfGen(std::unique_ptr<RWMol> &mol, unsigned int numConformers,
                       DGeomHelpers::EmbedParameters &dgParams) {
  MolOps::addHs(*mol);
  auto cids = DGeomHelpers::EmbedMultipleConfs(*mol, numConformers, dgParams);
  if (cids.empty() || !mol->getNumConformers()) {
    BOOST_LOG(rdErrorLog) << "Couldn't generate conformers for isomer "
                          << MolToCXSmiles(*mol) << " of molecule."
                          << std::endl;
    mol.reset();
  } else {
    MolOps::removeHs(*mol);
  }
}

}  // namespace
std::vector<std::unique_ptr<RWMol>> generateIsomerConformers(
    const ROMol &mol, unsigned int numConformers, bool enumerateStereo,
    const EnumerateStereoisomers::StereoEnumerationOptions &enumOpts,
    DGeomHelpers::EmbedParameters &dgParams,
    UserConfGenerator &userConfGenerator, unsigned int maxStereoCenters) {
  std::vector<std::unique_ptr<RWMol>> confMols;
  if (enumerateStereo) {
    EnumerateStereoisomers::StereoisomerEnumerator enu(mol, enumOpts);
    unsigned int i = 0;
    while (auto isomer = enu.next()) {
      confMols.emplace_back(static_cast<RWMol *>(isomer.release()));
      if (++i == maxStereoCenters) {
        break;
      }
    }
  } else {
    confMols.push_back(std::make_unique<RWMol>(mol));
  }
  for (auto &cm : confMols) {
    if (ControlCHandler::getGotSignal()) {
      confMols.clear();
      break;
    }
    if (userConfGenerator) {
      useUserConfGen(cm, numConformers, userConfGenerator);
    } else {
      useDefaultConfGen(cm, numConformers, dgParams);
    }
    if (ControlCHandler::getGotSignal()) {
      confMols.clear();
      return confMols;
    }
    if (cm && !cm->getNumConformers()) {
      cm.reset();
    }
  }
  confMols.erase(std::remove_if(confMols.begin(), confMols.end(),
                                [](const std::unique_ptr<RWMol> &m) -> bool {
                                  return !m;
                                }),
                 confMols.end());
  return confMols;
}

namespace {
// For deciphering the contents of the HoldsJoin property on 2 atoms.
// Used to decide the isotope number a dummy atom must be set to.
unsigned int joinInCommon(const std::string &joins1,
                          const std::string &joins2) {
  // Each of the join strings will be a underscore-separated list of ints, with
  // no more than 3 ints per string.  The ints will be between 1 and 4.
  // So we don't need to do anything sophisticated.  We want the single
  // number in common to both lists.
  std::string j(" ");
  for (auto c1 : joins1) {
    if (c1 == '_') {
      continue;
    }
    for (auto c2 : joins2) {
      if (c1 == c2) {
        j[0] = c1;
        return std::stoi(std::string(j));
      }
    }
  }
  return 0;
}

void splitDummyDummyBonds(RWMol &mol) {
  // Sometimes the atom subsetting leaves bonds between atoms not
  // in molNum.  Once setDummyIsotopes has been called, they will
  // be between dummy atoms and so can safely be removed.
  mol.beginBatchEdit();
  for (auto bond : mol.bonds()) {
    if (!bond->getBeginAtom()->getAtomicNum() &&
        !bond->getEndAtom()->getAtomicNum()) {
      mol.removeBond(bond->getBeginAtomIdx(), bond->getEndAtomIdx());
    }
  }
  mol.commitBatchEdit();
}
}  // namespace

std::unique_ptr<RWMol> trimSampleMol(const ROMol &mol, size_t molNum) {
  auto ts = MolToCXSmiles(mol);
  boost::dynamic_bitset<> molNumAtoms(mol.getNumAtoms());
  unsigned int molNumProp;
  for (auto atom : mol.atoms()) {
    if (atom->getPropIfPresent<unsigned int>("molNum", molNumProp) &&
        molNumProp == molNum) {
      molNumAtoms[atom->getIdx()] = true;
    }
  }

  auto ringInfo = mol.getRingInfo();
  if (!ringInfo->isInitialized()) {
    MolOps::findSSSR(mol);
  }
  for (const auto &atomRing : ringInfo->atomRings()) {
    for (auto a : atomRing) {
      if (molNumAtoms[a]) {
        for (auto ap : atomRing) {
          molNumAtoms[ap] = true;
        }
        break;
      }
    }
  }

  std::vector<unsigned int> bondsToGo;
  for (auto bond : mol.bonds()) {
    // Remove single non-cyclic bonds that aren't in molNumAtoms at either
    // end.
    if (bond->getBondType() == Bond::SINGLE &&
        !ringInfo->numBondRings(bond->getIdx())) {
      bool bondToGo = true;
      auto begAtom = bond->getBeginAtom();
      auto endAtom = bond->getEndAtom();
      if (molNumAtoms[begAtom->getIdx()] || molNumAtoms[endAtom->getIdx()]) {
        bondToGo = false;
      }
      // Deal with some special cases.  Don't break a S(=O)-[C,N] bond
      // as that gives an odd-looking [SH](=O) product.
      Atom *sAtom = nullptr;
      Atom *otherAtom = nullptr;
      if (bondToGo && begAtom->getAtomicNum() == 16) {
        sAtom = begAtom;
        otherAtom = endAtom;
      }
      if (bondToGo && endAtom->getAtomicNum() == 16) {
        sAtom = endAtom;
        otherAtom = begAtom;
      }
      if (sAtom &&
          (otherAtom->getAtomicNum() == 6 || otherAtom->getAtomicNum() == 7)) {
        unsigned int numOxy = 0;
        for (const auto nbr : mol.atomNeighbors(sAtom)) {
          if (nbr->getAtomicNum() == 8) {
            auto bond = mol.getBondBetweenAtoms(sAtom->getIdx(), nbr->getIdx());
            if (bond) {
              numOxy++;
            }
          }
        }
        if (numOxy) {
          bondToGo = false;
        }
      }
      if (bondToGo) {
        bondsToGo.push_back(bond->getIdx());
      }
    }
  }
  std::unique_ptr<RWMol> newMol(static_cast<RWMol *>(
      MolFragmenter::fragmentOnBonds(mol, bondsToGo, false)));
  std::vector<std::vector<int>> molFrags;
  auto numFrags = MolOps::getMolFrags(*newMol, molFrags);
  newMol->beginBatchEdit();
  for (unsigned int i = 0; i < numFrags; i++) {
    bool hoseFrag = true;
    for (auto fa : molFrags[i]) {
      auto a = newMol->getAtomWithIdx(fa);
      if (a->hasProp("molNum") &&
          a->getProp<unsigned int>("molNum") == molNum) {
        hoseFrag = false;
        break;
      }
    }
    if (hoseFrag) {
      for (auto fa : molFrags[i]) {
        newMol->removeAtom(fa);
      }
    }
  }
  newMol->commitBatchEdit();

  return newMol;
}

namespace {
void setJoinIsotope(
    const std::vector<std::tuple<Atom *, Atom *, unsigned int>> &atomIsotopes,
    std::vector<std::pair<unsigned int, double>> &dummyRadii) {
  for (auto &[atom, otherAtom, isotopeNum] : atomIsotopes) {
    atom->setAtomicNum(0);
    atom->setIsotope(isotopeNum);
    atom->setNumExplicitHs(0);
    dummyRadii.emplace_back(atom->getIdx(), GaussianShape::DUMMY_RAD);
  }
}

void duplicateJoinIsotope(
    RWMol &mol,
    const std::vector<std::tuple<Atom *, Atom *, unsigned int>> &atomIsotopes,
    std::vector<std::pair<unsigned int, double>> &dummyRadii,
    std::vector<unsigned int> &fragAtoms) {
  // Deal with something of an edge case.  The synthon was most likely a single
  // atom that finished a ring, so had 2 connection points e.g.
  // R1-[14C]([1*])[2*] and R2C([1*])CCC[2*] to give R1[14C]1C(R2)CCCC2. The
  // shape needs to have 2 dummy atoms, so not R1[*]1C(R2)CCCC2 but
  // R1[*]CCC[*]R2 with the 2 dummy atoms at the same coordinates.
  // This needs to be done last thing before the shapes are created as
  // it might break an aromatic system that will cause problems.
  for (auto &[atom, otherAtom, isotopeNum] : atomIsotopes) {
    if (!atom->getAtomicNum()) {
      if (atom->getIsotope() == isotopeNum) {
        continue;
      }
      Atom dummyAtom(0);
      auto newAtomIdx = mol.addAtom(&dummyAtom);
      auto newAtom = mol.getAtomWithIdx(newAtomIdx);
      newAtom->setIsotope(isotopeNum);
      // Remove the bond between this atom and otherAtom and make a bond
      // between new atom and otherAtom.
      auto bond = mol.getBondBetweenAtoms(atom->getIdx(), otherAtom->getIdx());
      auto bt = bond->getBondType();
      mol.removeBond(atom->getIdx(), otherAtom->getIdx());
      mol.addBond(otherAtom->getIdx(), newAtomIdx, bt);
      for (unsigned int i = 0; i < mol.getNumConformers(); i++) {
        auto &conf = mol.getConformer(i);
        auto atomPos = conf.getAtomPos(atom->getIdx());
        conf.setAtomPos(newAtomIdx, atomPos);
      }
      fragAtoms.push_back(newAtomIdx);
      dummyRadii.emplace_back(newAtomIdx, GaussianShape::DUMMY_RAD);
    }
  }
}

}  // namespace

void makeShapesFromMol(std::vector<std::unique_ptr<SampleMolRec>> &sampleMols,
                       std::atomic<std::int64_t> &mostRecentMol,
                       DGeomHelpers::EmbedParameters &dgParams,
                       ShapeBuildParams &shapeBuildParams,
                       std::unique_ptr<ProgressBar> &pbar) {
  while (!ControlCHandler::getGotSignal()) {
    size_t molNum = ++mostRecentMol;
    if (molNum >= sampleMols.size()) {
      return;
    }
    sampleMols[molNum]->d_mol = sampleMols[molNum]->d_synthonSet->buildMolecule(
        sampleMols[molNum]->d_synthonNums);
    if (!sampleMols[molNum]->d_mol) {
      continue;
    }
    sampleMols[molNum]->d_mol = trimSampleMol(
        *sampleMols[molNum]->d_mol, sampleMols[molNum]->d_synthonSetNum);
    constexpr unsigned int maxStereoCentresToDo =
        3;  // Don't enumerate more than 3 stereo centres
    auto isomerConfs = generateIsomerConformers(
        *sampleMols[molNum]->d_mol, shapeBuildParams.numConfs, true,
        shapeBuildParams.stereoEnumOpts, dgParams,
        shapeBuildParams.userConformerGenerator, maxStereoCentresToDo);
    if (isomerConfs.empty()) {
      // Sometimes we can get something from the un-enumerated isomers which
      // will be better than nothing.
      isomerConfs = generateIsomerConformers(
          *sampleMols[molNum]->d_mol, shapeBuildParams.numConfs, false,
          shapeBuildParams.stereoEnumOpts, dgParams,
          shapeBuildParams.userConformerGenerator, maxStereoCentresToDo);
      if (isomerConfs.empty()) {
        BOOST_LOG(rdWarningLog)
            << "No conformers generated for sample molecule "
            << sampleMols[molNum]->d_mol->getProp<std::string>(
                   common_properties::_Name)
            << " : " << MolToSmiles(*sampleMols[molNum]->d_mol)
            << " when generating conformers for synthon "
            << sampleMols[molNum]->d_synthon->getSmiles() << std::endl;
        continue;
      }
    }
    std::unique_ptr<SynthonShapeInput> allShapes;
    for (auto &isomer : isomerConfs) {
      if (isomer->getNumConformers() == 0) {
        continue;
      }
      std::vector<unsigned int> splitBonds;
      std::vector<unsigned int> fragAtoms;
      std::vector<std::pair<unsigned int, double>> dummyRadii;
      std::vector<std::tuple<Atom *, Atom *, unsigned int>> atomIsotopes;
      for (const auto &bond : isomer->bonds()) {
        if (!bond->hasProp("molNum")) {
          splitBonds.push_back(bond->getIdx());
          if (!bond->getBeginAtom()->hasProp("molNum") ||
              !bond->getEndAtom()->hasProp("molNum")) {
            // Probably an explicit H added by the stereoisomer enumerator.
            continue;
          }
          auto begMolNum =
              bond->getBeginAtom()->getProp<unsigned int>("molNum");
          auto endMolNum = bond->getEndAtom()->getProp<unsigned int>("molNum");
          if (begMolNum == sampleMols[molNum]->d_synthonSetNum &&
              endMolNum != sampleMols[molNum]->d_synthonSetNum) {
            fragAtoms.push_back(bond->getBeginAtomIdx());
            fragAtoms.push_back(bond->getEndAtomIdx());
            // Set the atom as a dummy, so it shows up in the shape.
            auto joinIsotope = joinInCommon(
                bond->getEndAtom()->getProp<std::string>("HoldsJoin"),
                bond->getBeginAtom()->getProp<std::string>("HoldsJoin"));
            atomIsotopes.emplace_back(bond->getEndAtom(), bond->getBeginAtom(),
                                      joinIsotope);
          } else if (begMolNum != sampleMols[molNum]->d_synthonSetNum &&
                     endMolNum == sampleMols[molNum]->d_synthonSetNum) {
            fragAtoms.push_back(bond->getBeginAtomIdx());
            fragAtoms.push_back(bond->getEndAtomIdx());
            // Set the atom as a dummy, so it shows up in the shape.
            auto joinIsotope = joinInCommon(
                bond->getEndAtom()->getProp<std::string>("HoldsJoin"),
                bond->getBeginAtom()->getProp<std::string>("HoldsJoin"));
            atomIsotopes.emplace_back(bond->getBeginAtom(), bond->getEndAtom(),
                                      joinIsotope);
          }
        } else {
          if (bond->getProp<unsigned int>("molNum") ==
              sampleMols[molNum]->d_synthonSetNum) {
            fragAtoms.push_back(bond->getBeginAtomIdx());
            fragAtoms.push_back(bond->getEndAtomIdx());
          }
        }
      }
      setJoinIsotope(atomIsotopes, dummyRadii);
      std::ranges::sort(fragAtoms);
      fragAtoms.erase(std::unique(fragAtoms.begin(), fragAtoms.end()),
                      fragAtoms.end());
      GaussianShape::ShapeInputOptions shapeOpts;
      shapeOpts.shapePruneThreshold = shapeBuildParams.shapeSimThreshold;
      // Make custom features for the atom subset.  Doing it from the
      // parent molecule means that the atom types will be correct which
      // may not be the case after the fragment is extracted.  There might
      // be a split aromatic ring, for example.
      shapeOpts.customFeatures.reserve(isomer->getNumConformers());
      for (auto cfi = isomer->beginConformers(); cfi != isomer->endConformers();
           ++cfi) {
        std::vector<GaussianShape::CustomFeature> feats;
        GaussianShape::findFeatures(*(*cfi), feats, fragAtoms);
        shapeOpts.customFeatures.emplace_back(std::move(feats));
      }
      duplicateJoinIsotope(*isomer, atomIsotopes, dummyRadii, fragAtoms);
      shapeOpts.atomRadii = dummyRadii;
      shapeOpts.atomSubset = fragAtoms;
      splitDummyDummyBonds(*isomer);
      try {
        GaussianShape::ShapeOverlayOptions ovlyOpts;
        if (!allShapes) {
          allShapes = std::make_unique<SynthonShapeInput>(*isomer, -1,
                                                          shapeOpts, ovlyOpts);
        } else {
          // Because stereoisomers should all have the same number of atoms and
          // bonds, we can just combine the shapes into one set.  We don't need
          // to keep track of which stereoisomer they came from.
          allShapes->merge(SynthonShapeInput(*isomer, -1, shapeOpts, ovlyOpts));
        }
      } catch (ValueErrorException &e) {
        // It throws an exception if it doesn't have a radius for an atom
        // in the molecule.
        BOOST_LOG(rdWarningLog) << e.what() << std::endl;
      } catch (Invar::Invariant &e) {
        std::ostringstream oss;
        oss << e.what() << "\n"
            << "No shapes generated for sample molecule "
            << sampleMols[molNum]->d_mol->getProp<std::string>(
                   common_properties::_Name)
            << " : " << MolToSmiles(*sampleMols[molNum]->d_mol)
            << " when generating conformers for synthon "
            << sampleMols[molNum]->d_synthon->getSmiles()
            << " with isomer : " << MolToSmiles(*isomer) << std::endl;
        throw std::runtime_error(oss.str());
      }
    }
    if (allShapes) {
      if (isomerConfs.size() > 1) {
        // allShapes will be the product of at least 1 merge, so prune.
        allShapes->getShapes().pruneShapes(shapeBuildParams.shapeSimThreshold);
      }
      sampleMols[molNum]->d_synthon->setShapes(std::move(allShapes));
    }
    if (pbar) {
      pbar->increment();
    }
  }
}

void makeShapesFromMols(std::vector<std::unique_ptr<SampleMolRec>> &sampleMols,
                        DGeomHelpers::EmbedParameters &dgParams,
                        ShapeBuildParams &shapeBuildParams,
                        std::unique_ptr<ProgressBar> &pbar) {
  std::atomic<std::int64_t> mostRecentMol = -1;
  if (const auto numThreadsToUse =
          getNumThreadsToUse(shapeBuildParams.numThreads);
      numThreadsToUse > 1) {
    std::vector<std::thread> threads;
    for (unsigned int i = 0u;
         i < std::min(static_cast<size_t>(numThreadsToUse), sampleMols.size());
         ++i) {
      threads.emplace_back(makeShapesFromMol, std::ref(sampleMols),
                           std::ref(mostRecentMol), std::ref(dgParams),
                           std::ref(shapeBuildParams), std::ref(pbar));
    }
    for (auto &t : threads) {
      t.join();
    }
  } else {
    makeShapesFromMol(sampleMols, mostRecentMol, dgParams,
                      std::ref(shapeBuildParams), pbar);
  }
}

void sortAndUniquifyToTry(
    std::vector<std::pair<const SynthonSpaceHitSet *, std::vector<size_t>>>
        &toTry) {
  // Uniquify on hit name, maintaining the order of the same name from
  // different hitsets.  This is particularly important for shape searching
  // where they should be in order of descending approx similarity.
  std::vector<std::pair<size_t, std::string>> tmp;
  tmp.reserve(toTry.size());
  for (size_t i = 0; i < toTry.size(); i++) {
    tmp.emplace_back(
        i, details::buildProductName(toTry[i].first, toTry[i].second));
  }
  std::stable_sort(tmp.begin(), tmp.end(),
                   [](const auto &lhs, const auto &rhs) -> bool {
                     return lhs.second < rhs.second;
                   });
  tmp.erase(std::unique(tmp.begin(), tmp.end(),
                        [](const auto &lhs, const auto &rhs) -> bool {
                          return lhs.second == rhs.second;
                        }),
            tmp.end());
  std::vector<std::pair<const SynthonSpaceHitSet *, std::vector<size_t>>>
      newToTry;
  newToTry.reserve(tmp.size());
  std::transform(tmp.begin(), tmp.end(), back_inserter(newToTry),
                 [&](const auto &p) -> auto { return toTry[p.first]; });
  toTry = std::move(newToTry);
}

}  // namespace RDKit::SynthonSpaceSearch::details
