#include "modules_discovery.hpp"

#include "../include/dv-sdk/cross/portable_io.h"

#include "log.hpp"

#include <boost/core/demangle.hpp>
#include <boost/filesystem.hpp>
#include <boost/tokenizer.hpp>

#include <algorithm>
#include <mutex>
#include <regex>
#include <typeinfo>
#include <unordered_set>
#include <vector>

#define INTERNAL_XSTR(a) INTERNAL_STR(a)
#define INTERNAL_STR(a)  #a

#define PATH_SEPARATOR_CHAR "|"

static dv::dvModuleComponents InternalLoadLibrary(const boost::filesystem::path &modulePath);

static struct {
	std::vector<boost::filesystem::path> modulePaths;
	std::vector<boost::filesystem::path> modulePathsHooks;
	std::mutex modulePathsMutex;
} glModuleData;

dv::dvModuleComponents dv::ModulesLoadLibrary(std::string_view moduleName) {
	// For each module, we search if a path exists to load it from.
	// If yes, we do so. The various OS's shared library load mechanisms
	// will keep track of reference count if same module is loaded
	// multiple times.
	boost::filesystem::path modulePath;

	{
		std::scoped_lock lock(glModuleData.modulePathsMutex);

		for (const auto &path : glModuleData.modulePaths) {
			if (moduleName == path.stem().string()) {
				// Found a module with same name!
				modulePath = path;
				break;
			}
		}
	}

	if (modulePath.empty()) {
		throw std::runtime_error(fmt::format(FMT_STRING("No module library for '{:s}' found."), moduleName));
	}

	return (InternalLoadLibrary(modulePath));
}

static dv::dvModuleComponents InternalLoadLibrary(const boost::filesystem::path &modulePath) {
#if BOOST_HAS_DLL_LOAD
	dv::ModuleLibrary moduleLibrary;
	try {
		moduleLibrary.load(modulePath.c_str(), boost::dll::load_mode::rtld_now);
	}
	catch (const std::exception &ex) {
		// Failed to load shared library!
		throw std::system_error(std::error_code(),
			fmt::format(FMT_STRING("Failed to load library '{:s}', error: '{:s}'. If this is an outdated "
								   "DV module, recompile it with the latest headers first."),
				modulePath.string(), ex.what()));
	}

	std::function<dvModuleInfo(void)> getInfo;
	try {
		getInfo = moduleLibrary.get<dvModuleInfo(void)>("dvModuleGetInfo170");
	}
	catch (const std::exception &ex) {
		// Failed to find symbol in shared library!
		// Maybe an old DV module?
		if (moduleLibrary.has("dvModuleGetInfo") || moduleLibrary.has("dvModuleGetInfo150")) {
			dv::ModulesUnloadLibrary(moduleLibrary);
			throw std::runtime_error(fmt::format(
				FMT_STRING("This is an outdated DV module, please recompile it with the latest headers to make it work "
						   "again. Symbol 'dvModuleGetInfo170' not present in library '{:s}', error: '{:s}'."),
				modulePath.string(), ex.what()));
		}

		dv::ModulesUnloadLibrary(moduleLibrary);
		throw std::range_error(
			fmt::format(FMT_STRING("Failed to find symbol 'dvModuleGetInfo170' in library '{:s}', error: '{:s}'."),
				modulePath.string(), ex.what()));
	}

	std::function<void *(enum dvModuleHooks)> getHooks;
	try {
		getHooks = moduleLibrary.get<void *(enum dvModuleHooks)>("dvModuleGetHooks");
	}
	catch (const std::exception &) {
		// Optional functionality, ignore exceptions thrown.
	}
#else
	dv::ModuleLibrary moduleLibrary = dlopen(modulePath.c_str(), RTLD_NOW);
	if (moduleLibrary == nullptr) {
		// Failed to load shared library!
		throw std::system_error(std::error_code(),
			fmt::format(FMT_STRING("Failed to load library '{:s}', error: '{:s}'. If this is an outdated "
								   "DV module, recompile it with the latest headers first."),
				modulePath.string(), dlerror()));
	}

	std::function<dvModuleInfo(void)> getInfo
		= reinterpret_cast<dvModuleInfo (*)(void)>(dlsym(moduleLibrary, "dvModuleGetInfo170"));
	if (!getInfo) {
		// Failed to find symbol in shared library!
		dv::ModulesUnloadLibrary(moduleLibrary);
		throw std::range_error(
			fmt::format(FMT_STRING("Failed to find symbol 'dvModuleGetInfo170' in library '{:s}', error: '{:s}'. If "
								   "this is an outdated DV module, recompile it with the latest headers first."),
				modulePath.string(), dlerror()));
	}

	std::function<void *(enum dvModuleHooks)> getHooks
		= reinterpret_cast<void *(*) (enum dvModuleHooks)>(dlsym(moduleLibrary, "dvModuleGetHooks"));
	// Optional functionality, ignore nullptr return.
#endif

	dvModuleInfo info = getInfo();
	if (info == nullptr) {
		dv::ModulesUnloadLibrary(moduleLibrary);
		throw std::runtime_error(
			fmt::format(FMT_STRING("Failed to get info from library '{:s}'."), modulePath.string()));
	}

	dv::dvModuleComponents result;
	result.library = moduleLibrary;
	result.info    = info;
	result.hooks   = getHooks;

	return result;
}

// Small helper to unload libraries on error.
void dv::ModulesUnloadLibrary(dv::ModuleLibrary &moduleLibrary) {
#if defined(DISABLE_SHARED_UNLOAD)
	UNUSED_ARGUMENT(moduleLibrary);
#else
#	if BOOST_HAS_DLL_LOAD
	moduleLibrary.unload();
#	else
	dlclose(moduleLibrary);
#	endif
#endif
}

void dv::ModulesUpdateInformationListener(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
	const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
	UNUSED_ARGUMENT(userData);

	if ((event == DVCFG_ATTRIBUTE_MODIFIED) && (changeType == DVCFG_TYPE_BOOL)
		&& (std::string{changeKey} == "updateModulesInformation") && changeValue.boolean) {
		// Get information on available modules, put it into ConfigTree.
		ModulesUpdateInformation();

		dvConfigNodeAttributeBooleanReset(node, changeKey);
	}
}

static inline std::vector<boost::filesystem::path> findModuleCandidates(const boost::filesystem::path &searchPath) {
	std::vector<boost::filesystem::path> moduleCandidates;

	// Search is recursive for binary shared libraries.
#if defined(OS_MACOS)
	const std::regex moduleRegex("\\w[\\w-]*\\.dylib");
#elif defined(OS_WINDOWS)
	const std::regex moduleRegex("\\w[\\w-]*\\.dll");
#else
	const std::regex moduleRegex("\\w[\\w-]*\\.so");
#endif

	if (boost::filesystem::exists(searchPath) && boost::filesystem::is_directory(searchPath)) {
		std::for_each(boost::filesystem::recursive_directory_iterator(searchPath),
			boost::filesystem::recursive_directory_iterator(),
			[&moduleRegex, &moduleCandidates](const boost::filesystem::directory_entry &e) {
				if (boost::filesystem::exists(e.path()) && boost::filesystem::is_regular_file(e.path())
					&& std::regex_match(e.path().filename().string(), moduleRegex)) {
					moduleCandidates.push_back(boost::filesystem::canonical(e.path()).make_preferred());
				}
			});
	}

	dv::vectorSortUnique(moduleCandidates);

	return (moduleCandidates);
}

static inline void cleanupModulesSearchPath(
	std::string searchCopy, const boost::filesystem::path &path, dv::Cfg::Node mNode) {
	// Ensure this is not part of the config setting.
	auto idx = searchCopy.find(path.string());

	// Not found.
	if (idx == std::string::npos) {
		return;
	}

	// Found, remove and update: +1 to also remove possible '|' char.
	searchCopy.erase(idx, path.string().length() + 1);

	mNode.put<dv::CfgType::STRING>("modulesSearchPath", searchCopy);
}

void dv::ModulesUpdateInformation() {
	std::scoped_lock lock(glModuleData.modulePathsMutex);

	auto modulesNode = dv::Cfg::GLOBAL.getNode("/system/modules/");

	// Clear out modules information.
	modulesNode.clearSubTree(false);
	modulesNode.removeSubTree();
	glModuleData.modulePaths.clear();
	glModuleData.modulePathsHooks.clear();

	// Search for available modules.
	std::vector<boost::filesystem::path> modules;

	const auto modulesSearchPath = modulesNode.get<dv::CfgType::STRING>("modulesSearchPath");

	// First all the additional paths in order.
	{
		// Split on '|'.
		boost::tokenizer<boost::char_separator<char>> additionalSearchPaths(
			modulesSearchPath, boost::char_separator<char>(PATH_SEPARATOR_CHAR, nullptr));

		for (const auto &path : additionalSearchPaths) {
			const auto candidates = findModuleCandidates(path);

			modules.insert(modules.end(), candidates.begin(), candidates.end());
		}
	}

	// Then the runtime location.
	{
		boost::filesystem::path execPath{dv::portable_get_executable_location()};

		if (!boost::filesystem::is_directory(execPath)) {
			execPath.remove_filename();
		}

		execPath.append("dv_modules");

		const auto candidates = findModuleCandidates(execPath);

		modules.insert(modules.end(), candidates.begin(), candidates.end());

		cleanupModulesSearchPath(modulesSearchPath, execPath, modulesNode);
	}

	// And last the default system modules directory.
	{
		const boost::filesystem::path modulesDefaultDir{INTERNAL_XSTR(DV_MODULES_DIR)};

		const auto candidates = findModuleCandidates(modulesDefaultDir);

		modules.insert(modules.end(), candidates.begin(), candidates.end());

		cleanupModulesSearchPath(modulesSearchPath, modulesDefaultDir, modulesNode);
	}

	// Stable sort by file name, to support multiple modules with same name.
	std::stable_sort(
		modules.begin(), modules.end(), [](const boost::filesystem::path &a, const boost::filesystem::path &b) {
			return (a.stem() < b.stem());
		});

	// Generate nodes for each module, with their in/out information as attributes.
	// This also checks basic validity of the module's information.
	std::unordered_set<std::string> seen;

	for (const auto &path : modules) {
		// Get name from path.
		const auto moduleName = path.stem().string();

		// Load library.
		dv::dvModuleComponents mLoad;

		try {
			mLoad = InternalLoadLibrary(path);
		}
		catch (const std::range_error &ex) {
			dv::Log(
				dv::logLevel::DEBUG, "Module candidate '{:s}': {:s} (probably not a DV module)", moduleName, ex.what());

			continue;
		}
		catch (const std::exception &ex) {
			dv::Log(dv::logLevel::ERROR, "Module candidate '{:s}': {:s}", moduleName, ex.what());

			continue;
		}

		// Get node under /system/modules/.
		auto moduleNode = modulesNode.getRelativeNode(moduleName + "/");

		if (seen.count(moduleName) == 0) {
			// First time we see this module, parse it into ConfigTree.
			moduleNode.create<dv::CfgType::INT>("version", mLoad.info->version, {0, INT32_MAX},
				dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Module version.");
			moduleNode.create<dv::CfgType::STRING>("description", mLoad.info->description, {1, 8192},
				dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Module description.");
			moduleNode.create<dv::CfgType::STRING>("path", path.string(), {1, PATH_MAX},
				dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Module file full path.");
			moduleNode.create<dv::CfgType::STRING>("alternativePaths", "", {0, 8 * PATH_MAX},
				dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Alternative module file paths (not loaded).");

			// Add to lists, module is valid.
			seen.insert(moduleName);

			glModuleData.modulePaths.push_back(path);

			// Does it have hooks available?
			moduleNode.create<dv::CfgType::BOOL>("hasHooks", static_cast<bool>(mLoad.hooks), {},
				dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Module has hooks interface.");

			if (mLoad.hooks) {
				glModuleData.modulePathsHooks.push_back(path);
			}
		}
		else {
			// Module already seen and parsed. Add to alternative unused paths.
			auto currAltPaths = moduleNode.getString("alternativePaths");

			moduleNode.updateReadOnly<dv::CfgType::STRING>("alternativePaths",
				(currAltPaths.empty()) ? (path.string()) : (currAltPaths + PATH_SEPARATOR_CHAR + path.string()));
		}

		// Done, unload library.
		dv::ModulesUnloadLibrary(mLoad.library);
	}

	// No modules, cannot start!
	if (glModuleData.modulePaths.empty()) {
		dv::Log(
			dv::logLevel::ERROR, "Failed to find any modules (additional search paths = '{:s}').", modulesSearchPath);
	}
	else {
		dv::Log(dv::logLevel::DEBUG, "Found {:d} modules (additional search paths = '{:s}').",
			glModuleData.modulePaths.size(), modulesSearchPath);
	}

	glModuleData.modulePaths.shrink_to_fit();
}

void dv::ModulesExecuteHook(enum dvModuleHooks hook, const std::function<void(void *)> &hookManagerFunction) {
	std::scoped_lock lock(glModuleData.modulePathsMutex);

	for (const auto &modulePath : glModuleData.modulePathsHooks) {
		// Get name from path.
		const auto moduleName = modulePath.stem().string();

		// Load library.
		dv::dvModuleComponents mLoad;

		try {
			mLoad = InternalLoadLibrary(modulePath);
		}
		catch (const std::exception &ex) {
			dv::Log(dv::logLevel::ERROR, "Module candidate '{:s}': {:s}", moduleName, ex.what());

			continue;
		}

		try {
			// Call hooks handling function. Must be present and valid since
			// we only loop over modules that were loaded successfully and
			// had a hook-handler declared.
			auto result = mLoad.hooks(hook);

			// Allow user to manage the returned value (generic void pointer).
			if ((result != nullptr) && hookManagerFunction) {
				hookManagerFunction(result);
			}
		}
		catch (const std::exception &ex) {
			dv::Log(dv::logLevel::ERROR, "dv::ModulesExecuteHook(): '{:s} :: {:s}'.",
				boost::core::demangle(typeid(ex).name()), ex.what());
		}

		// Done, unload library.
		dv::ModulesUnloadLibrary(mLoad.library);
	}
}
