#include <algorithm>
#include <iostream>
#include <stdexcept>
#include <comparatorLibs/comparatorApiManager.h>
#include <loadableLib/loadableLibraryManager.h>
#include <opts.h>
#include <user/comparatorApiDesc.h>
#include <user/smoHooks.h>

namespace smo {
namespace comparator_lib {

namespace {

void registerExportedComparatorTypes(
	ComparatorApiManager::Resources& resources,
	const cologex::ComparatorLibDesc& libDesc)
{
	for (const auto& exportedType : libDesc.exportedComparatorTypes)
	{
		const uint64_t packedTypeId = exportedType.typeId.pack();
		if (resources.typeRegistry.contains(packedTypeId))
		{
			throw std::runtime_error(
				std::string(__func__)
				+ ": Duplicate comparator type registration for vendorId=0x"
				+ std::to_string(exportedType.typeId.vendorId)
				+ " typeId=0x"
				+ std::to_string(exportedType.typeId.typeId));
		}

		auto registeredType =
			std::make_shared<cologex::ExportedComparatorTypeDesc>(
				exportedType);
		resources.typeRegistry.emplace(packedTypeId, registeredType);
	}
}

void unregisterExportedComparatorTypes(
	ComparatorApiManager::Resources& resources,
	const cologex::ComparatorLibDesc& libDesc)
{
	for (const auto& exportedType : libDesc.exportedComparatorTypes) {
		resources.typeRegistry.erase(exportedType.typeId.pack());
	}
}

} // namespace

ComparatorLib& ComparatorApiManager::loadComparatorLib(
	const std::string& libraryPath)
{
	loadable_lib::LoadableLibraryManager& llm =
		loadable_lib::LoadableLibraryManager::getInstance();

	if (getComparatorLib(libraryPath))
	{
		throw std::runtime_error(
			std::string(__func__) + ": ComparatorLib already loaded: "
			+ libraryPath);
	}

	std::shared_ptr<loadable_lib::LoadableLibraryManager::LoadedSharedLibrary>
		loadedLibrary = llm.loadSharedLibrary(libraryPath);

	auto descFn = loadable_lib::LoadableLibraryManager::resolveSymbol<
		SMO_GET_COMPARATOR_LIB_DESC_FN_TYPEDEF *>(
		loadedLibrary->getDlopenHandle(),
		SMO_GET_COMPARATOR_LIB_DESC_FN_NAME_STR);

	const stim_buff::SmoCallbacks& callbacks =
		stim_buff::getSmoCallbacks();
	const cologex::ComparatorLibDesc& libDesc = descFn(callbacks);

	auto lib = std::make_shared<ComparatorLib>(loadedLibrary, descFn);
	lib->setComparatorLibDesc(libDesc);

	registerExportedComparatorTypes(s.rsrc, lib->comparatorLibDesc);
	s.rsrc.libs.push_back(lib);

	return *lib;
}

void ComparatorApiManager::unloadComparatorLib(const std::string& libraryPath)
{
	auto& libs = s.rsrc.libs;
	auto it = std::find_if(
		libs.begin(), libs.end(),
		[&libraryPath](const std::shared_ptr<ComparatorLib>& lib) {
			return lib->loadedSharedLibrary->libraryPath == libraryPath;
		});

	if (it == libs.end())
	{
		std::cerr << std::string(__func__) + ": Library not found: "
			<< libraryPath << '\n';
		return;
	}

	unregisterExportedComparatorTypes(s.rsrc, (*it)->comparatorLibDesc);
	std::shared_ptr<loadable_lib::LoadableLibraryManager::LoadedSharedLibrary>
		loadedLibrary = (*it)->loadedSharedLibrary;
	libs.erase(it);

	loadable_lib::LoadableLibraryManager::getInstance()
		.unloadSharedLibrary(loadedLibrary);
}

void ComparatorApiManager::unloadAllComparatorLibs(void)
{
	std::vector<std::shared_ptr<
		loadable_lib::LoadableLibraryManager::LoadedSharedLibrary>>
		loadedLibrariesTmp;
	loadedLibrariesTmp.reserve(s.rsrc.libs.size());

	for (const auto& lib : s.rsrc.libs) {
		loadedLibrariesTmp.push_back(lib->loadedSharedLibrary);
	}

	s.rsrc.libs.clear();
	s.rsrc.typeRegistry.clear();

	loadable_lib::LoadableLibraryManager& llm =
		loadable_lib::LoadableLibraryManager::getInstance();
	for (const auto& loadedLibrary : loadedLibrariesTmp) {
		llm.unloadSharedLibrary(loadedLibrary);
	}
}

void ComparatorApiManager::loadAllComparatorApiLibsFromOptions(void)
{
	const auto& options = OptionParser::getOptions();
	for (const auto& libPath : options.comparatorLibs) {
		loadComparatorLib(libPath);
	}
}

std::optional<std::shared_ptr<ComparatorLib>>
ComparatorApiManager::getComparatorLib(const std::string& libraryPath)
{
	auto& libs = s.rsrc.libs;
	auto it = std::find_if(
		libs.begin(), libs.end(),
		[&libraryPath](const std::shared_ptr<ComparatorLib>& lib) {
			return lib->loadedSharedLibrary->libraryPath == libraryPath;
		});

	if (it != libs.end()) {
		return *it;
	}

	return std::nullopt;
}

std::shared_ptr<cologex::ExportedComparatorTypeDesc>
ComparatorApiManager::getComparatorType(cologex::ComparatorTypeId typeId)
{
	const uint64_t packedTypeId = typeId.pack();
	auto it = s.rsrc.typeRegistry.find(packedTypeId);
	if (it == s.rsrc.typeRegistry.end()) {
		return {};
	}

	return it->second;
}

std::unique_ptr<cologex::Comparator> ComparatorApiManager::getNewComparatorInstance(
	cologex::ComparatorTypeId typeId)
{
	return getNewComparatorInstance(getComparatorType(typeId));
}

std::unique_ptr<cologex::Comparator> ComparatorApiManager::getNewComparatorInstance(
	const std::shared_ptr<cologex::ExportedComparatorTypeDesc>& comparatorType)
{
	if (!comparatorType)
	{
		throw std::runtime_error(
			std::string(__func__) + ": comparatorType is null");
	}

	if (!comparatorType->getNewInstance)
	{
		throw std::runtime_error(
			std::string(__func__) + ": getNewInstance is null");
	}

	return comparatorType->getNewInstance();
}

std::string ComparatorApiManager::stringifyLibs() const
{
	std::string result;
	for (const auto& lib : s.rsrc.libs)
	{
		if (!result.empty()) {
			result += "\n";
		}
		result += lib->stringify();
	}
	return result;
}

} // namespace comparator_lib
} // namespace smo
