//
//  Copyright (c) 2008, Novartis Institutes for BioMedical Research Inc.
//  All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials provided
//       with the distribution.
//     * Neither the name of Novartis Institutes for BioMedical Research Inc.
//       nor the names of its contributors may be used to endorse or promote
//       products derived from this software without specific prior
//       written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Created by Greg Landrum, September 2006
//
#include <RDGeneral/export.h>
#ifndef __RD_SLNPARSEOPS_H__
#define __RD_SLNPARSEOPS_H__

#include <vector>
#include <GraphMol/SLNParse/SLNParse.h>
#include <GraphMol/SLNParse/SLNAttribs.h>
#include <GraphMol/RDKitBase.h>
#include <GraphMol/RDKitQueries.h>
#include <RDGeneral/BoostStartInclude.h>
#include <boost/lexical_cast.hpp>
#include <RDGeneral/BoostEndInclude.h>

namespace RDKit {
namespace SLNParse {
namespace {
//!  set a bookmark in the molecule if the atom has an associated ID:
void bookmarkAtomID(RWMol *mp, Atom *atom) {
  PRECONDITION(mp, "bad molecule");
  PRECONDITION(atom, "bad atom");
  unsigned int label;
  if (atom->getPropIfPresent(common_properties::_AtomID, label)) {
    if (mp->hasAtomBookmark(label)) {
      std::stringstream err;
      err << "SLN Parser error: Atom ID " << label << " used a second time.";
      throw SLNParseException(err.str());
    }
    if (mp->hasBondBookmark(label)) {
      std::stringstream err;
      err << "SLN Parser error: Atom ID " << label
          << " appears *after* its ring closure.";
      throw SLNParseException(err.str());
    }
    mp->setAtomBookmark(atom, label);
  }
}

//! adds a bond, being careful to handle aromaticity properly
template <typename BondType>
void addBondToMol(RWMol *mp, BondType *bond) {
  PRECONDITION(mp, "null molecule");
  PRECONDITION(bond, "null bond");
  mp->addBond(bond, true);
  if (bond->getBondType() == Bond::AROMATIC) {
    // SLN doesn't have aromatic atom types, aromaticity is a property
    // of the bonds themselves, so we need to set the atom types:
    bond->setIsAromatic(true);
    bond->getBeginAtom()->setIsAromatic(true);
    bond->getEndAtom()->setIsAromatic(true);
  }
}
}  // end of anonymous namespace

// ------------------------------------------------------------------------------------
//! initialize a molecule
template <typename AtomType>
int startMol(std::vector<RWMol *> &molList, AtomType *firstAtom,
             bool doingQuery) {
  PRECONDITION(firstAtom, "empty atom");
  RWMol *mp = new RWMol();
  mp->addAtom(firstAtom, true, true);
  bookmarkAtomID(mp, firstAtom);

  if (!doingQuery) {
    // add any hydrogens that are set on the atom, otherwise getting the
    // numbering right
    // is just too hard:
    for (unsigned int i = 0; i < firstAtom->getNumExplicitHs(); ++i) {
      int hIdx = mp->addAtom(new Atom(1), false, true);
      mp->addBond(0, hIdx, Bond::SINGLE);
    }
    firstAtom->setNumExplicitHs(0);
  }

  int sz = molList.size();
  molList.push_back(mp);
  return sz;
};

// ------------------------------------------------------------------------------------
//! adds an atom to a molecule
template <typename AtomType, typename BondType>
void addAtomToMol(std::vector<RWMol *> &molList, unsigned int idx,
                  AtomType *atom, BondType *bond, bool doingQuery) {
  PRECONDITION(idx < molList.size(), "bad index");
  RWMol *mp = molList[idx];
  PRECONDITION(mp, "null molecule");
  PRECONDITION(atom, "empty atom");
  PRECONDITION(bond, "null bond");

  Atom *a1 = mp->getActiveAtom();
  int atomIdx1 = a1->getIdx();
  int atomIdx2 = mp->addAtom(atom, true, true);
  bookmarkAtomID(mp, atom);
  bond->setOwningMol(mp);
  bond->setBeginAtomIdx(atomIdx1);
  bond->setEndAtomIdx(atomIdx2);
  addBondToMol(mp, bond);

  if (!doingQuery) {
    // add any hydrogens that are set on the atom, otherwise getting the
    // numbering right
    // is just too hard:
    for (unsigned int i = 0; i < atom->getNumExplicitHs(); ++i) {
      int hIdx = mp->addAtom(new Atom(1), false, true);
      mp->addBond(atomIdx2, hIdx, Bond::SINGLE);
    }
    atom->setNumExplicitHs(0);
  }
}
//! \overload
template <typename AtomType>
void addAtomToMol(std::vector<RWMol *> &molList, unsigned int idx,
                  AtomType *atom, bool doingQuery) {
  addAtomToMol(molList, idx, atom, new Bond(Bond::SINGLE), doingQuery);
}

// ------------------------------------------------------------------------------------
//! closes an indexed ring in a molecule using the bond provided
// The bond is formed from the atom in the molecule with the
// corresponding bookmark to the active atom
//
template <typename BondType>
void closeRingBond(std::vector<RWMol *> &molList, unsigned int molIdx,
                   unsigned int ringIdx, BondType *bond,
                   bool postponeAllowed = true) {
  PRECONDITION(molIdx < molList.size(), "bad index");
  RWMol *mp = molList[molIdx];
  PRECONDITION(mp, "null molecule");
  PRECONDITION(bond, "Null bond");

  if (!mp->hasAtomBookmark(ringIdx)) {
    if (postponeAllowed) {
      // save it for later:
      bond->setOwningMol(mp);
      bond->setEndAtomIdx(mp->getActiveAtom()->getIdx());
      mp->setBondBookmark(bond, ringIdx);
      return;
    } else {
      std::stringstream err;
      err << "SLN Parser error: Ring closure " << ringIdx
          << " does not have a corresponding opener.";
      throw SLNParseException(err.str());
    }
  }
  Atom *opener = mp->getAtomWithBookmark(ringIdx);
  CHECK_INVARIANT(opener, "invalid atom");

  Atom *closer = mp->getActiveAtom();
  bond->setOwningMol(mp);
  bond->setBeginAtom(opener);
  bond->setEndAtom(closer);
  addBondToMol(mp, bond);
};
//! \overload
void closeRingBond(std::vector<RWMol *> &molList, unsigned int molIdx,
                   unsigned int ringIdx) {
  auto *newBond = new Bond(Bond::SINGLE);
  try {
    closeRingBond(molList, molIdx, ringIdx, newBond);
  } catch (...) {
    delete newBond;
    throw;
  }
};

// ------------------------------------------------------------------------------------
// NOTE: this takes over responsibility for the bond
template <typename BondType>
int addBranchToMol(std::vector<RWMol *> &molList, unsigned int molIdx,
                   unsigned int branchIdx, BondType *&bond) {
  PRECONDITION(molIdx < molList.size(), "bad index");
  RWMol *mp = molList[molIdx];
  PRECONDITION(mp, "null molecule");
  PRECONDITION(branchIdx < molList.size(), "bad index");
  RWMol *branch = molList[branchIdx];
  PRECONDITION(branch, "null branch");
  PRECONDITION(bond, "null bond");

  unsigned int activeAtomIdx = mp->getActiveAtom()->getIdx();
  unsigned int nOrigAtoms = mp->getNumAtoms();

  //
  // Add the fragment's atoms and bonds to the molecule:
  //
  mp->insertMol(*branch);

  // copy in any atom bookmarks from the branch:
  for (ROMol::ATOM_BOOKMARK_MAP::const_iterator bmIt =
           branch->getAtomBookmarks()->begin();
       bmIt != branch->getAtomBookmarks()->end(); ++bmIt) {
    if (bmIt->first < 0) continue;
    if (mp->hasAtomBookmark(bmIt->first)) {
      std::stringstream err;
      err << "SLN Parser error: Atom ID " << bmIt->first
          << " used a second time.";
      throw SLNParseException(err.str());
    } else if (mp->hasBondBookmark(bmIt->first)) {
      std::stringstream err;
      err << "SLN Parser error: Atom ID " << bmIt->first
          << " appears *after* its ring closure.";
      throw SLNParseException(err.str());
    } else {
      CHECK_INVARIANT(bmIt->second.size() == 1,
                      "bad atom bookmark list on branch");
      Atom *tgtAtom =
          mp->getAtomWithIdx((*bmIt->second.begin())->getIdx() + nOrigAtoms);
      mp->setAtomBookmark(tgtAtom, bmIt->first);
    }
  }

  // loop over bond bookmarks in the branch and close the corresponding rings
  for (ROMol::BOND_BOOKMARK_MAP::const_iterator bmIt =
           branch->getBondBookmarks()->begin();
       bmIt != branch->getBondBookmarks()->end(); ++bmIt) {
    CHECK_INVARIANT(bmIt->second.size() >= 1,
                    "bad bond bookmark list on branch");
    for (ROMol::BOND_PTR_LIST::const_iterator bondIt = bmIt->second.begin();
         bondIt != bmIt->second.end(); ++bondIt) {
      Bond *tgtBond = *bondIt;
      if (bmIt->first > 0 && mp->hasAtomBookmark(bmIt->first)) {
        Atom *tmpAtom = mp->getActiveAtom();
        mp->setActiveAtom(
            mp->getAtomWithIdx(tgtBond->getEndAtomIdx() + nOrigAtoms));
        closeRingBond(molList, molIdx, bmIt->first, tgtBond, false);
        mp->setActiveAtom(tmpAtom);
      } else {
        // no partner found yet, copy into this mol:
        tgtBond->setOwningMol(mp);
        tgtBond->setEndAtomIdx(tgtBond->getEndAtomIdx() + nOrigAtoms);
        mp->setBondBookmark(tgtBond, bmIt->first);
      }
    }
  }

  // set the connecting bond:
  if (bond->getBondType() != Bond::IONIC) {
    bond->setOwningMol(mp);
    bond->setBeginAtomIdx(activeAtomIdx);
    bond->setEndAtomIdx(nOrigAtoms);
    addBondToMol(mp, bond);
  } else {
    delete bond;
  }
  bond = 0;

  delete branch;
  unsigned int sz = molList.size();
  if (sz == branchIdx + 1) {
    molList.resize(sz - 1);
  }
  return molIdx;
};
//! \overload
int addBranchToMol(std::vector<RWMol *> &molList, unsigned int molIdx,
                   unsigned int branchIdx) {
  Bond *newBond = new Bond(Bond::SINGLE);
  int ret = -1;
  try {
    ret = addBranchToMol(molList, molIdx, branchIdx, newBond);
  } catch (...) {
    delete newBond;
    throw;
  }
  return ret;
};

// ------------------------------------------------------------------------------------
//! adds the atoms and bonds from a fragment to the molecule, sets no bond
// between them
int addFragToMol(std::vector<RWMol *> &molList, unsigned int molIdx,
                 unsigned int fragIdx) {
  Bond *newBond = new Bond(Bond::IONIC);
  int ret = -1;
  try {
    ret = addBranchToMol(molList, molIdx, fragIdx, newBond);
  } catch (...) {
    delete newBond;
    throw;
  }
  return ret;
}

//! convenience function to convert the argument to a string
template <typename T>
std::string convertToString(T val) {
  std::string res = boost::lexical_cast<std::string>(val);
  return res;
}

}  // end of namespace SLNParse
}  // end of namespace RDKit
#endif
