/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2020 NKI/AVL, Netherlands Cancer Institute
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer
 * 2. 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.
 *
 * 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.
 */

#include "pdb-redo/Compound.hpp"

#include "cif++/datablock.hpp"
#include "cif++/utilities.hpp"
#include "pdb-redo/Version.hpp"

#include <algorithm>
#include <exception>
#include <filesystem>
#include <fstream>
#include <map>
#include <mutex>
#include <stdexcept>
#include <utility>

namespace fs = std::filesystem;

namespace pdb_redo
{

using cif::atom_type_traits;
using std::numbers::pi;

// --------------------------------------------------------------------
// Compound helper classes

struct CompoundAtomLess
{
	bool operator()(const CompoundAtom &a, const CompoundAtom &b) const
	{
		int d = a.id.compare(b.id);
		if (d == 0)
			d = a.typeSymbol - b.typeSymbol;
		return d < 0;
	}
};

struct CompoundBondLess
{
	bool operator()(const CompoundBond &a, const CompoundBond &b) const
	{
		int d = a.atomID[0].compare(b.atomID[0]);
		if (d == 0)
			d = a.atomID[1].compare(b.atomID[1]);
		if (d == 0)
			d = a.type - b.type;
		return d < 0;
	}
};

// --------------------------------------------------------------------
// Compound

Compound::Compound(const cif::datablock &db, const std::string &id,
	std::string name, std::string group)
	: mCF(db)
	, mID(id)
	, mName(std::move(name))
	, mGroup(std::move(group))
{
	try
	{
		auto &compoundAtoms = db["chem_comp_atom"];

		for (auto row : compoundAtoms)
		{
			std::string atom_id, symbol, energy;
			float charge, x, y, z;

			cif::tie(atom_id, symbol, energy, charge, x, y, z) = row.get("atom_id", "type_symbol", "type_energy", "partial_charge", "x", "y", "z");

			mAtoms.push_back({ atom_id, atom_type_traits(symbol).type(), energy, charge, x, y, z });
		}
		std::ranges::sort(mAtoms, CompoundAtomLess());

		auto &compBonds = db["chem_comp_bond"];

		for (auto row : compBonds)
		{
			CompoundBond b;
			std::optional<std::string> valueOrder, aromatic;

			cif::tie(b.atomID[0], b.atomID[1], valueOrder, aromatic, b.distance, b.esd) =
				row.get("atom_id_1", "atom_id_2", "value_order", "pdbx_aromatic_flag", "value_dist", "value_dist_esd");

			// Such a briliant idea, to rename columns in an CIF file...

			if (not valueOrder)
				cif::tie(valueOrder) = row.get("type");

			if (not aromatic)
				cif::tie(aromatic) = row.get("aromatic");

			b.aromatic = cif::iequals(aromatic.value_or("N"), "Y");

			// ... and not only once, but even multiple times

			if (not aromatic)
				cif::tie(aromatic) = row.get("aromat");

			using cif::iequals;

			if (not valueOrder)
			{
				if (cif::VERBOSE > 0)
					std::cerr << "Missing value_order in chem_comp_bond for " << mID << '\n';
				b.type = singleBond;
			}
			else if (iequals(*valueOrder, "single"))
				b.type = singleBond;
			else if (iequals(*valueOrder, "double"))
				b.type = doubleBond;
			else if (iequals(*valueOrder, "triple"))
				b.type = tripleBond;
			else if (b.aromatic)
				b.type = aromaticBond;
			else if (iequals(*valueOrder, "deloc"))
				b.type = delocalizedBond;
			else
			{
				if (cif::VERBOSE > 0)
					std::cerr << "Unimplemented chem_comp_bond.value_order " << *valueOrder << " in " << mID << '\n';
				b.type = singleBond;
			}

			if (b.atomID[0] > b.atomID[1])
				swap(b.atomID[0], b.atomID[1]);

			mBonds.push_back(b);
		}
		std::ranges::sort(mBonds, CompoundBondLess());

		for (auto row : db["chem_comp_angle"])
		{
			CompoundAngle a;

			cif::tie(a.atomID[0], a.atomID[1], a.atomID[2], a.angle, a.esd) =
				row.get("atom_id_1", "atom_id_2", "atom_id_3", "value_angle", "value_angle_esd");

			mAngles.push_back(a);
		}

		for (auto row : db["chem_comp_tor"])
		{
			CompoundTorsion a;

			cif::tie(a.atomID[0], a.atomID[1], a.atomID[2], a.atomID[3], a.angle, a.esd, a.period) =
				row.get("atom_id_1", "atom_id_2", "atom_id_3", "atom_id_4", "value_angle", "value_angle_esd", "period");

			mTorsions.push_back(a);
		}

		for (auto row : db["chem_comp_chir"])
		{
			CompoundChiralCentre cc;
			std::string volumeSign;

			cif::tie(cc.id, cc.atomIDCentre, cc.atomID[0],
				cc.atomID[1], cc.atomID[2], volumeSign) =
				row.get("id", "atom_id_centre", "atom_id_1",
					"atom_id_2", "atom_id_3", "volume_sign");

			if (volumeSign == "negativ" or volumeSign == "negative")
				cc.volumeSign = negativ;
			else if (volumeSign == "positiv" or volumeSign == "positive")
				cc.volumeSign = positiv;
			else if (volumeSign == "both")
				cc.volumeSign = both;
			else
			{
				if (cif::VERBOSE > 0)
					std::cerr << "Unimplemented chem_comp_chir.volume_sign " << volumeSign << " in " << id << '\n';
				continue;
			}

			mChiralCentres.push_back(cc);
		}

		auto &compPlanes = db["chem_comp_plane_atom"];

		for (auto row : compPlanes)
		{
			std::string atom_id, plane_id;
			float esd;

			cif::tie(atom_id, plane_id, esd) = row.get("atom_id", "plane_id", "dist_esd");

			auto i = std::ranges::find_if(mPlanes, [&](auto &p)
				{ return p.id == plane_id; });
			if (i == mPlanes.end())
				mPlanes.emplace_back(CompoundPlane{ plane_id, { atom_id }, esd });
			else
				i->atomID.push_back(atom_id);
		}
	}
	catch (const std::exception &ex)
	{
		std::cerr << "Error loading ccp4 data for " << id << '\n';
		throw;
	}
}

std::string Compound::formula() const
{
	std::string result;

	std::map<std::string, uint32_t> atoms;
	float chargeSum = 0;

	for (auto r : mAtoms)
	{
		atoms[atom_type_traits(r.typeSymbol).symbol()] += 1;
		chargeSum += r.partialCharge;
	}

	auto c = atoms.find("C");
	if (c != atoms.end())
	{
		result = "C";

		if (c->second > 1)
			result += std::to_string(c->second);

		atoms.erase(c);

		auto h = atoms.find("H");
		if (h != atoms.end())
		{
			result += " H";
			if (h->second > 1)
				result += std::to_string(h->second);

			atoms.erase(h);
		}
	}

	for (auto a : atoms)
	{
		if (not result.empty())
			result += ' ';

		result += a.first;
		if (a.second > 1)
			result += std::to_string(a.second);
	}

	int charge = lrint(chargeSum);
	if (charge != 0)
		result += ' ' + std::to_string(charge);

	return result;
}

float Compound::formulaWeight() const
{
	float result = 0;

	for (auto r : mAtoms)
		result += atom_type_traits(r.typeSymbol).weight();

	return result;
}

int Compound::charge() const
{
	float result = 0;

	for (auto r : mAtoms)
		result += r.partialCharge;

	return lrint(result);
}

std::string Compound::type() const
{
	std::string result;

	// known groups are (counted from ccp4 monomer dictionary)

	//	D-pyranose
	//	DNA
	//	L-PEPTIDE LINKING
	//	L-SACCHARIDE
	//	L-peptide
	//	L-pyranose
	//	M-peptide
	//	NON-POLYMER
	//	P-peptide
	//	RNA
	//	furanose
	//	non-polymer
	//	non_polymer
	//	peptide
	//	pyranose
	//	saccharide

	if (cif::iequals(mID, "gly"))
		result = "peptide linking";
	else if (cif::iequals(mGroup, "l-peptide") or cif::iequals(mGroup, "L-peptide linking") or cif::iequals(mGroup, "peptide"))
		result = "L-peptide linking";
	else if (cif::iequals(mGroup, "DNA"))
		result = "DNA linking";
	else if (cif::iequals(mGroup, "RNA"))
		result = "RNA linking";
	else
		result = mGroup;

	return result;
}

bool Compound::isWater() const
{
	return mID == "HOH" or mID == "H2O";
}

bool Compound::isSugar() const
{
	return cif::iequals(mGroup, "furanose") or cif::iequals(mGroup, "pyranose");
}

CompoundAtom Compound::get_atom_by_atom_id(const std::string &atomID) const
{
	CompoundAtom result = {};
	for (auto &a : mAtoms)
	{
		if (a.id == atomID)
		{
			result = a;
			break;
		}
	}

	if (result.id != atomID)
		throw std::out_of_range("No atom " + atomID + " in Compound " + mID);

	return result;
}

bool Compound::atomsBonded(const std::string &atomId_1, const std::string &atomId_2) const
{
	auto i = std::ranges::find_if(mBonds,
		[&](const CompoundBond &b)
		{
			return (b.atomID[0] == atomId_1 and b.atomID[1] == atomId_2) or (b.atomID[0] == atomId_2 and b.atomID[1] == atomId_1);
		});

	return i != mBonds.end();
}

float Compound::atomBondValue(const std::string &atomId_1, const std::string &atomId_2) const
{
	auto i = std::ranges::find_if(mBonds,
		[&](const CompoundBond &b)
		{
			return (b.atomID[0] == atomId_1 and b.atomID[1] == atomId_2) or (b.atomID[0] == atomId_2 and b.atomID[1] == atomId_1);
		});

	return i != mBonds.end() ? i->distance : 0;
}

// bool Compound::isIsomerOf(const Compound &c) const
// {
// 	bool result = false;

// 	for (;;)
// 	{
// 		// easy tests first
// 		if (mID == c.mID)
// 		{
// 			result = true;
// 			break;
// 		}

// 		if (mAtoms.size() != c.mAtoms.size())
// 			break;

// 		if (mBonds.size() != c.mBonds.size())
// 			break;

// 		if (mChiralCentres.size() != c.mChiralCentres.size())
// 			break;

// 		// same number of atoms of each type?
// 		std::map<cif::atom_type, int> aTypeCount, bTypeCount;

// 		bool sameAtomNames = true;
// 		for (std::size_t i = 0; i < mAtoms.size(); ++i)
// 		{
// 			auto &a = mAtoms[i];
// 			auto &b = c.mAtoms[i];

// 			aTypeCount[a.typeSymbol] += 1;
// 			bTypeCount[b.typeSymbol] += 1;

// 			if (a.id != b.id or a.typeSymbol != b.typeSymbol)
// 				sameAtomNames = false;
// 		}

// 		if (not sameAtomNames and aTypeCount != bTypeCount)
// 			break;

// 		bool sameBonds = sameAtomNames;
// 		for (std::size_t i = 0; sameBonds and i < mBonds.size(); ++i)
// 		{
// 			sameBonds =
// 				mBonds[i].atomID[0] == c.mBonds[i].atomID[0] and
// 				mBonds[i].atomID[1] == c.mBonds[i].atomID[1] and
// 				mBonds[i].type == c.mBonds[i].type;
// 		}

// 		if (sameBonds)
// 		{
// 			result = true;
// 			break;
// 		}

// 		// implement rest of tests

// 		std::vector<std::tuple<std::string, std::string>> mapping;
// 		result = StructuresAreIsomeric(mAtoms, mBonds, c.mAtoms, c.mBonds, mapping);

// 		if (cif::VERBOSE and result)
// 		{
// 			for (auto &m : mapping)
// 				std::cerr << "  " << std::get<0>(m) << " => " << std::get<1>(m) << '\n';
// 		}

// 		break;
// 	}

// 	return result;
// }

// std::vector<std::tuple<std::string, std::string>> Compound::mapToIsomer(const Compound &c) const
// {
// 	std::vector<std::tuple<std::string, std::string>> result;

// 	bool check = StructuresAreIsomeric(mAtoms, mBonds, c.mAtoms, c.mBonds, result);
// 	if (not check)
// 		throw std::runtime_error("Compounds " + id() + " and " + c.id() + " are not isomers in call to mapToIsomer");

// 	return result;
// }

// std::vector<std::string> Compound::isomers() const
// {
// 	std::vector<std::string> result;

// 	auto &db = IsomerDB::instance();
// 	if (db.count(mID))
// 	{
// 		result = db[mID];

// 		auto i = find(result.begin(), result.end(), mID);
// 		assert(i != result.end());

// 		result.erase(i);
// 	}

// 	return result;
// }

float Compound::bondAngle(const std::string &atomId_1, const std::string &atomId_2, const std::string &atomId_3) const
{
	float result = nanf("1");

	for (auto &a : mAngles)
	{
		if (not(a.atomID[1] == atomId_2 and
				((a.atomID[0] == atomId_1 and a.atomID[2] == atomId_3) or
					(a.atomID[2] == atomId_1 and a.atomID[0] == atomId_3))))
			continue;

		result = a.angle;
		break;
	}

	return result;
}

// static float calcC(float a, float b, float alpha)
//{
//	float f = b * std::sin(alpha * std::numbers::pi_v<float> / 180);
//	float d = std::sqrt(b * b - f * f);
//	float e = a - d;
//	float c = std::sqrt(f * f + e * e);
//
//	return c;
// }

float Compound::chiralVolume(const std::string &centreID) const
{
	float result = 0;

	for (auto &cv : mChiralCentres)
	{
		if (cv.id != centreID)
			continue;

		// calculate the expected chiral volume

		// the edges

		float a = atomBondValue(cv.atomIDCentre, cv.atomID[0]);
		float b = atomBondValue(cv.atomIDCentre, cv.atomID[1]);
		float c = atomBondValue(cv.atomIDCentre, cv.atomID[2]);

		// the angles for the top of the tetrahedron

		float alpha = bondAngle(cv.atomID[0], cv.atomIDCentre, cv.atomID[1]);
		float beta = bondAngle(cv.atomID[1], cv.atomIDCentre, cv.atomID[2]);
		float gamma = bondAngle(cv.atomID[2], cv.atomIDCentre, cv.atomID[0]);

		auto cosa = static_cast<float>(std::cos(alpha * std::numbers::pi_v<float> / 180));
		auto cosb = static_cast<float>(std::cos(beta * std::numbers::pi_v<float> / 180));
		auto cosc = static_cast<float>(std::cos(gamma * std::numbers::pi_v<float> / 180));

		// When the atoms are in a plane and the result should be nearly zero
		// the result of 1 + 2 * cosa * cosb * cosc - cosa^2 - cosb^2 - cosc^2 can become negative
		// and thus give a nan.

		if (auto v = 1 + 2 * cosa * cosb * cosc - (cosa * cosa) - (cosb * cosb) - (cosc * cosc); v > 0)
			result = (a * b * c * std::sqrt(v)) / 6;
		else
			result = 0;

		if (cv.volumeSign == negativ)
			result = -result;

		break;
	}

	return result;
}

// --------------------------------------------------------------------

Link::Link(cif::datablock &db)
{
	mID = db.name();
	if (cif::starts_with(mID, "link_"))
		mID.erase(mID.begin(), mID.begin() + 5);

	auto &linkBonds = db["chem_link_bond"];

	for (auto row : linkBonds)
	{
		LinkBond b;
		std::string type;

		cif::tie(b.atom[0].compID, b.atom[0].atomID,
			b.atom[1].compID, b.atom[1].atomID, type, b.distance, b.esd) =
			row.get("atom_1_comp_id", "atom_id_1", "atom_2_comp_id", "atom_id_2",
				"type", "value_dist", "value_dist_esd");

		using cif::iequals;

		if (iequals(type, "single") or iequals(type, "sing"))
			b.type = singleBond;
		else if (iequals(type, "double") or iequals(type, "doub"))
			b.type = doubleBond;
		else if (iequals(type, "triple") or iequals(type, "trip"))
			b.type = tripleBond;
		else if (iequals(type, "deloc") or iequals(type, "aromat") or iequals(type, "aromatic"))
			b.type = delocalizedBond;
		else
		{
			if (cif::VERBOSE > 0)
				std::cerr << "Unimplemented chem_link_bond.type " << type << " in " << mID << '\n';
			b.type = singleBond;
		}

		//		if (b.atom[0] > b.atom[1])
		//			swap(b.atom[0], b.atom[1]);

		mBonds.push_back(b);
	}
	//	sort(mBonds.begin(), mBonds.end(), LinkBondLess());

	auto &linkAngles = db["chem_link_angle"];
	for (auto row : linkAngles)
	{
		LinkAngle a;

		cif::tie(a.atom[0].compID, a.atom[0].atomID, a.atom[1].compID, a.atom[1].atomID,
			a.atom[2].compID, a.atom[2].atomID, a.angle, a.esd) =
			row.get("atom_1_comp_id", "atom_id_1", "atom_2_comp_id", "atom_id_2",
				"atom_3_comp_id", "atom_id_3", "value_angle", "value_angle_esd");

		mAngles.push_back(a);
	}

	for (auto row : db["chem_link_tor"])
	{
		LinkTorsion a;

		cif::tie(a.atom[0].compID, a.atom[0].atomID, a.atom[1].compID, a.atom[1].atomID,
			a.atom[2].compID, a.atom[2].atomID, a.atom[3].compID, a.atom[3].atomID,
			a.angle, a.esd, a.period) =
			row.get("atom_1_comp_id", "atom_id_1", "atom_2_comp_id", "atom_id_2",
				"atom_3_comp_id", "atom_id_3", "atom_4_comp_id", "atom_id_4",
				"value_angle", "value_angle_esd", "period");

		mTorsions.push_back(a);
	}

	auto &linkChir = db["chem_link_chir"];
	for (auto row : linkChir)
	{
		LinkChiralCentre cc;
		std::string volumeSign;

		cif::tie(cc.id, cc.atomCentre.compID, cc.atomCentre.atomID,
			cc.atom[0].compID, cc.atom[0].atomID,
			cc.atom[1].compID, cc.atom[1].atomID,
			cc.atom[2].compID, cc.atom[2].atomID,
			volumeSign) =
			row.get("id", "atom_centre_comp_id", "atom_id_centre",
				"atom_1_comp_id", "atom_id_1", "atom_2_comp_id", "atom_id_2",
				"atom_3_comp_id", "atom_id_3", "volume_sign");

		if (volumeSign == "negativ" or volumeSign == "negative")
			cc.volumeSign = negativ;
		else if (volumeSign == "positiv" or volumeSign == "positive")
			cc.volumeSign = positiv;
		else if (volumeSign == "both")
			cc.volumeSign = both;
		else
		{
			if (cif::VERBOSE > 0)
				std::cerr << "Unimplemented chem_link_chir.volume_sign " << volumeSign << " in " << mID << '\n';
			continue;
		}

		mChiralCentres.push_back(cc);
	}

	auto &linkPlanes = db["chem_link_plane"];
	for (auto row : linkPlanes)
	{
		int compID;
		std::string atomID, planeID;
		float esd;

		cif::tie(planeID, compID, atomID, esd) = row.get("plane_id", "atom_comp_id", "atom_id", "dist_esd");

		auto i = std::ranges::find_if(mPlanes, [&](auto &p)
			{ return p.id == planeID; });
		if (i == mPlanes.end())
		{
			std::vector<LinkAtom> atoms{ LinkAtom{ compID, atomID } };
			mPlanes.emplace_back(LinkPlane{ planeID, std::move(atoms), esd });
		}
		else
			i->atoms.push_back({ compID, atomID });
	}
}

float Link::atomBondValue(const LinkAtom &atom1, const LinkAtom &atom2) const
{
	auto i = std::ranges::find_if(mBonds,
		[&](auto &b)
		{
			return (b.atom[0] == atom1 and b.atom[1] == atom2) or (b.atom[0] == atom2 and b.atom[1] == atom1);
		});

	return i != mBonds.end() ? i->distance : 0;
}

float Link::bondAngle(const LinkAtom &atom1, const LinkAtom &atom2, const LinkAtom &atom3) const
{
	float result = nanf("1");

	for (auto &a : mAngles)
	{
		if (not(a.atom[1] == atom2 and
				((a.atom[0] == atom1 and a.atom[2] == atom3) or
					(a.atom[2] == atom1 and a.atom[0] == atom3))))
			continue;

		result = a.angle;
		break;
	}

	return result;
}

float Link::chiralVolume(const std::string &centreID, const std::string &compound_id_1, const std::string &compound_id_2) const
{
	float result = 0;

	auto bondValue = [&](const LinkAtom &a, const LinkAtom &b) -> float
	{
		float result = atomBondValue(a, b);

		if (result == 0)
		{
			if (a.compID != b.compID)
				throw std::runtime_error("cannot calculate chiral volume since bond lengths are missing");

			auto cmp = CompoundFactory::instance().create(a.compID == 1 ? compound_id_1 : compound_id_2);

			if (cmp == nullptr)
				throw std::runtime_error("cannot calculate chiral volume since compound is not known");

			result = cmp->atomBondValue(a.atomID, b.atomID);
		}

		return result;
	};

	auto angle = [&](const LinkAtom &a, const LinkAtom &b, const LinkAtom &c) -> float
	{
		float result = bondAngle(a, b, c);

		if (std::isnan(result))
		{
			if (a.compID != b.compID or a.compID != c.compID)
				throw std::runtime_error("cannot calculate chiral volume since bond lengths are missing");

			auto cmp = CompoundFactory::instance().create(a.compID == 1 ? compound_id_1 : compound_id_2);

			if (cmp == nullptr)
				throw std::runtime_error("cannot calculate chiral volume since compound is not known");

			result = cmp->bondAngle(a.atomID, b.atomID, c.atomID);
		}

		return result;
	};

	for (auto &cv : mChiralCentres)
	{
		if (cv.id != centreID)
			continue;

		// calculate the expected chiral volume

		// the edges

		float a = bondValue(cv.atomCentre, cv.atom[0]);
		float b = bondValue(cv.atomCentre, cv.atom[1]);
		float c = bondValue(cv.atomCentre, cv.atom[2]);

		// the angles for the top of the tetrahedron

		float alpha = angle(cv.atom[0], cv.atomCentre, cv.atom[1]);
		float beta = angle(cv.atom[1], cv.atomCentre, cv.atom[2]);
		float gamma = angle(cv.atom[2], cv.atomCentre, cv.atom[0]);

		auto cosa = static_cast<float>(std::cos(alpha * std::numbers::pi_v<float> / 180));
		auto cosb = static_cast<float>(std::cos(beta * std::numbers::pi_v<float> / 180));
		auto cosc = static_cast<float>(std::cos(gamma * std::numbers::pi_v<float> / 180));

		// When the atoms are in a plane and the result should be nearly zero
		// the result of 1 + 2 * cosa * cosb * cosc - cosa^2 - cosb^2 - cosc^2 can become negative
		// and thus give a nan.

		if (auto v = 1 + 2 * cosa * cosb * cosc - (cosa * cosa) - (cosb * cosb) - (cosc * cosc); v > 0)
			result = (a * b * c * std::sqrt(v)) / 6;
		else
			result = 0;

		if (cv.volumeSign == negativ)
			result = -result;

		break;
	}

	return result;
}

// --------------------------------------------------------------------

class CompoundFactoryImpl
{
  public:
	CompoundFactoryImpl(CompoundFactoryImpl *inNext = nullptr)
		: mNext(inNext)
	{
	}

	CompoundFactoryImpl(std::istream &inData, CompoundFactoryImpl *inNext = nullptr)
		: CompoundFactoryImpl(cif::file(inData))
	{
	}

	CompoundFactoryImpl(cif::file file, CompoundFactoryImpl *inNext = nullptr)
		: mNext(inNext)
		, mFile(std::move(file))
	{
	}

	virtual ~CompoundFactoryImpl()
	{
		delete mNext;
	}

	const Compound *create(std::string id);
	virtual const Compound *createSelf(std::string id) = 0;

	const Link *createLink(std::string id);

	CompoundFactoryImpl *pop()
	{
		auto result = mNext;
		mNext = nullptr;
		delete this;
		return result;
	}

  protected:
	CompoundFactoryImpl *mNext = nullptr;
	std::mutex mMutex;
	cif::file mFile;
	std::vector<std::unique_ptr<const Compound>> mCompounds;
	std::vector<std::unique_ptr<const Link>> mLinks;
};

const Compound *CompoundFactoryImpl::create(std::string id)
{
	std::unique_lock lock(mMutex);

	cif::to_upper(id);

	const Compound *result = nullptr;

	for (auto &cmp : mCompounds)
	{
		if (cif::iequals(cmp->id(), id))
		{
			result = cmp.get();
			break;
		}
	}

	if (result == nullptr)
	{
		try
		{
			result = createSelf(id);
		}
		catch (const std::exception &ex)
		{
			if (cif::VERBOSE > 0)
				std::cout << ex.what() << "\n";
		}
	}

	if (result == nullptr and mNext != nullptr)
		result = mNext->create(id);

	return result;
}

const Link *CompoundFactoryImpl::createLink(std::string id)
{
	std::unique_lock lock(mMutex);

	cif::to_upper(id);

	const Link *result = nullptr;

	for (auto &link : mLinks)
	{
		if (cif::iequals(link->id(), id))
		{
			result = link.get();
			break;
		}
	}

	if (result == nullptr and mFile.contains("link_" + id))
		result = mLinks.emplace_back(std::make_unique<Link>(mFile["link_" + id])).get();

	if (result == nullptr and mNext != nullptr)
		result = mNext->createLink(id);

	return result;
}

// --------------------------------------------------------------------

class RestraintCompoundFactoryImpl : public CompoundFactoryImpl
{
  public:
	RestraintCompoundFactoryImpl(cif::file file, CompoundFactoryImpl *inNext)
		: CompoundFactoryImpl(std::move(file), inNext)
	{
		cif::file cf;

		for (auto &&id : mFile["comp_list"]["chem_comp"].rows<std::string>("id"))
		{
			auto c = createSelf(id);

			if (c == nullptr)
				throw std::runtime_error("Error creating restraint for " + id);

			// Only forward compounds that are not known yet
			// if (cif::compound_factory::instance().exists(id))
			// 	continue;

			cf.emplace_back(c->generateCCDCompound());
		}

		cif::compound_factory::instance().push_dictionary(cf);
	}

	~RestraintCompoundFactoryImpl() override
	{
		cif::compound_factory::instance().pop_dictionary();
	}

	const Compound *createSelf(std::string id) override;
};

const Compound *RestraintCompoundFactoryImpl::createSelf(std::string id)
{
	const Compound *result = nullptr;

	if (not mFile.empty())
	{
		auto &cat = mFile["comp_list"]["chem_comp"];

		auto rs = cat.find(cif::key("three_letter_code") == id);

		if (not rs.empty())
		{
			auto row = rs.front();

			std::string name, group;
			uint32_t numberAtomsAll, numberAtomsNh;
			cif::tie(name, group, numberAtomsAll, numberAtomsNh) =
				row.get("name", "group", "number_atoms_all", "number_atoms_nh");

			cif::trim(name);
			cif::trim(group);

			if (mFile.contains("comp_" + id))
			{
				mCompounds.emplace_back(new Compound(mFile["comp_" + id], id, name, group));
				result = mCompounds.back().get();
			}
		}
	}

	return result;
}

// --------------------------------------------------------------------

class CLibdMonCompoundFactoryImpl : public CompoundFactoryImpl
{
  public:
	CLibdMonCompoundFactoryImpl() = default;

	CLibdMonCompoundFactoryImpl(std::istream &inData, CompoundFactoryImpl *inNext)
		: CompoundFactoryImpl(inData, inNext)
	{
	}

	const Compound *createSelf(std::string id) override;

  private:
	std::set<std::string> mMissing;
};

const Compound *CLibdMonCompoundFactoryImpl::createSelf(std::string id)
{
	const Compound *result = nullptr;
	if (auto clibd_mon_path = getenv("CLIBD_MON"); clibd_mon_path and not mFile.empty() and not mMissing.contains(id))
	{
		auto clibd_mon = fs::path(clibd_mon_path);

		fs::path resFile = clibd_mon / cif::to_lower_copy(id.substr(0, 1)) / (id + ".cif");
		cif::file cifFile(resFile);

		auto &cat = cifFile["comp_list"]["chem_comp"];

		auto rs = cat.find(cif::key("three_letter_code") == id);

		if (not rs.empty())
		{
			auto row = rs.front();

			std::string name, group;
			uint32_t numberAtomsAll, numberAtomsNh;
			cif::tie(name, group, numberAtomsAll, numberAtomsNh) =
				row.get("name", "group", "number_atoms_all", "number_atoms_nh");

			cif::trim(name);
			cif::trim(group);

			if (cifFile.contains("comp_" + id))
			{
				mCompounds.emplace_back(new Compound(cifFile["comp_" + id], id, name, group));
				result = mCompounds.back().get();
			}
		}
	}
	else
		std::cerr << "Is the CCP4 environment sourced?\n";

	return result;
}

std::string Compound::getDescriptor(std::string_view type) const
{
	auto cat = mCF.get("pdbx_chem_comp_descriptor");
	if (cat == nullptr)
		throw std::runtime_error("pdbx_chem_comp_descriptor is missing in restraints file for " + mID);

	return cat->find1<std::string>(cif::key("type") == type, "descriptor");
}

// --------------------------------------------------------------------

struct TypeMapping
{
	std::string restr_type, ccd_type;
};
const TypeMapping kTypeMap[] = { // NOLINT
	{ "DNA", "DNA linking" },
	{ "furanose", "saccharide" },
	{ "ketopyranose", "saccharide" },
	{ "M-peptide", "peptide linking" },
	{ "NON-POLYMER", "non-polymer" },
	{ "peptide", "peptide linking" },
	{ "P-peptide", "peptide linking" },
	{ "pyranose", "saccharide" },
	{ "RNA", "RNA linking" }
};

cif::datablock Compound::generateCCDCompound() const
{
	using namespace cif::literals;

	cif::datablock result{ mID };

	auto &chemCompCCD = result["chem_comp"];
	auto &chemCompAtomCCD = result["chem_comp_atom"];
	auto &chemCompBondCCD = result["chem_comp_bond"];

	std::string compType;
	for (auto &[restrType, ccdType] : kTypeMap)
	{
		if (cif::iequals(restrType, mGroup))
		{
			compType = ccdType;
			break;
		}
	}

	if (compType.empty())
		throw std::runtime_error("Unknown type in restraint file: " + mGroup);

	std::optional<std::string> unknown;
	std::optional<std::string> oneLetterCode;
	if (auto i = cif::compound_factory::kAAMap.find(mID); i != cif::compound_factory::kAAMap.end())
		oneLetterCode = { i->second };

	std::optional<std::string> threeLetterCode;
	if (mID.length() == 3)
		threeLetterCode = mID;

	bool pdbx_ideal_coordinates_missing_flag = true;

	// --------------------------------------------------------------------
	int formalCharge = 0;

	auto &chemCompAtom = mCF["chem_comp_atom"];
	for (int nr = 1; auto row : chemCompAtom)
	{
		std::string atom_id, type_symbol;
		std::optional<int> atom_charge;
		float atom_x, atom_y, atom_z;

		cif::tie(atom_id, type_symbol, atom_charge, atom_x, atom_y, atom_z) =
			row.get("atom_id", "type_symbol", "charge", "x", "y", "z");

		chemCompAtomCCD.emplace({ { "comp_id", mID },
			{ "atom_id", atom_id },
			{ "alt_atom_id", atom_id },
			{ "type_symbol", type_symbol },
			{ "charge", atom_charge },
			{ "pdbx_align", unknown },
			{ "pdbx_aromatic_flag", unknown },
			{ "pdbx_leaving_atom_flag", unknown },
			{ "pdbx_stereo_config", unknown },
			{ "pdbx_backbone_atom_flag", unknown },
			{ "pdbx_n_terminal_atom_flag", unknown },
			{ "pdbx_c_terminal_atom_flag", unknown },
			{ "model_Cartn_x", atom_x },
			{ "model_Cartn_y", atom_y },
			{ "model_Cartn_z", atom_z },
			{ "pdbx_model_Cartn_x_ideal", unknown },
			{ "pdbx_model_Cartn_y_ideal", unknown },
			{ "pdbx_model_Cartn_z_ideal", unknown },
			{ "pdbx_component_atom_id", unknown },
			{ "pdbx_component_comp_id", unknown },
			{ "pdbx_ordinal", nr++ } });

		if (atom_charge)
			formalCharge += *atom_charge;
	}

	for (int nr = 1; auto bond : mBonds)
	{
		std::string valueOrder;
		switch (bond.type)
		{
			case singleBond: valueOrder = "SING"; break;
			case doubleBond: valueOrder = "DOUB"; break;
			case tripleBond: valueOrder = "TRIP"; break;
			case aromaticBond: valueOrder = "AROM"; break;
			case delocalizedBond: valueOrder = "DELO"; break;
		}

		chemCompBondCCD.emplace({ { "comp_id", mID },
			{ "atom_id_1", bond.atomID[0] },
			{ "atom_id_2", bond.atomID[1] },
			{ "value_order", valueOrder },
			{ "pdbx_aromatic_flag", bond.aromatic },
			{ "pdbx_stereo_config", unknown },
			{ "pdbx_ordinal", nr++ } });
	}

	// --------------------------------------------------------------------

	chemCompCCD.emplace({
		// clang-format off
		{ "id", mID },
		{ "name", mName },
		{ "type", compType },
		{ "pdbx_type", unknown },
		{ "formula", formula() },
		{ "mon_nstd_parent_comp_id", unknown },
		{ "pdbx_synonyms", unknown },
		{ "pdbx_formal_charge", formalCharge },
		{ "pdbx_initial_date", unknown },
		{ "pdbx_modified_date", unknown },
		{ "pdbx_ambiguous_flag", unknown },
		{ "pdbx_release_status", unknown },
		{ "pdbx_replaced_by", unknown },
		{ "pdbx_replaces", unknown },
		{ "formula_weight", formulaWeight() },
		{ "one_letter_code", oneLetterCode },
		{ "three_letter_code", threeLetterCode },
		{ "pdbx_model_coordinates_details", unknown },
		{ "pdbx_model_coordinates_missing_flag", false },
		{ "pdbx_ideal_coordinates_details", unknown },
		{ "pdbx_ideal_coordinates_missing_flag", pdbx_ideal_coordinates_missing_flag },
		{ "pdbx_model_coordinates_db_code", unknown },
		{ "pdbx_subcomponent_list", unknown },
		{ "pdbx_processing_site", unknown },
		{ "pdbx_pcm", unknown },
		// clang-format on
	});

	return result;
}

// --------------------------------------------------------------------

CompoundFactory::CompoundFactory()
{
	fs::path mon_lib_list;
	if (const char *clibdMon = getenv("CLIBD_MON"))
		mon_lib_list = fs::path(clibdMon) / "list" / "mon_lib_list.cif";

	std::ifstream file(mon_lib_list);

	if (mon_lib_list.empty() or not fs::exists(mon_lib_list) or not file.is_open())
	{
		if (cif::VERBOSE > 0)
			std::cerr << cif::coloured("Could not load the mon_lib_list.cif file from CCP4, please make sure you have installed CCP4 and sourced the environment.", cif::colour::white, cif::colour::red) << '\n';
		mImpl = new CLibdMonCompoundFactoryImpl();
	}
	else
		mImpl = new CLibdMonCompoundFactoryImpl(file, nullptr);

	force_link = 42;
}

CompoundFactory::~CompoundFactory()
{
	delete mImpl;
}

CompoundFactory &CompoundFactory::instance()
{
	static CompoundFactory sInstance;
	return sInstance;
}

void CompoundFactory::pushDictionary(const fs::path &inDictFile)
{
	if (not fs::exists(inDictFile))
		throw std::runtime_error("file not found: " + inDictFile.string());

	try
	{
		std::ifstream file(inDictFile);
		if (not file.is_open())
			throw std::system_error(errno, std::generic_category(), inDictFile.string());

		mImpl = new RestraintCompoundFactoryImpl(cif::file(file), mImpl);
	}
	catch (const std::exception &ex)
	{
		std::cerr << "Error loading dictionary " << inDictFile << '\n';
		throw;
	}
}

void CompoundFactory::pushDictionary(std::istream &is)
{
	mImpl = new RestraintCompoundFactoryImpl(cif::file(is), mImpl);
}

void CompoundFactory::pushDictionary(cif::file file)
{
	mImpl = new RestraintCompoundFactoryImpl(std::move(file), mImpl);
}

void CompoundFactory::popDictionary()
{
	assert(mImpl != nullptr);
	if (mImpl != nullptr)
		mImpl = mImpl->pop();

	// should not pop the last one!
	assert(mImpl != nullptr);
}

// id is the three letter code
const Compound *CompoundFactory::create(std::string id)
{
	return mImpl->create(id);
}

const Link *CompoundFactory::createLink(std::string id)
{
	return mImpl->createLink(id);
}

} // namespace pdb_redo