#include <iostream>
#include <stdexcept>
#include <algorithm>
#include <filesystem>
#include <loadableLib/loadableLibraryManager.h>
#include <opts.h>

namespace fs = std::filesystem;

namespace smo {
namespace loadable_lib {

namespace {

std::vector<std::string> buildSmoLibrarySearchPaths()
{
	std::vector<std::string> searchPaths = {
		fs::current_path().string(),
		fs::path("/proc/self/exe").parent_path().string()
	};

	const auto& options = OptionParser::getOptions();
	if (!options.senseApiLibPath.empty())
	{
		searchPaths.insert(
			searchPaths.begin(),
			options.senseApiLibPath.begin(),
			options.senseApiLibPath.end());
	}

	return searchPaths;
}

void logLibraryNotInSearchPaths(
	const std::string& libraryPath,
	const std::vector<std::string>& searchPaths)
{
	std::cerr << "searchForLibInSmoSearchPaths: library '"
		<< libraryPath << "' isn't in search bespoke search paths: ";

	for (const auto& path : searchPaths) {
		std::cerr << path << " ";
	}
	std::cerr << std::endl;
	std::cerr << "Trying to load " << libraryPath
		<< " from system default search paths\n";
}

std::unique_ptr<void, LoadableLibraryManager::SharedLibraryDeleter>
tryDlopenPath(const std::string& path)
{
	dlerror();
	void* handle = dlopen(path.c_str(), RTLD_LAZY);
	if (!handle) {
		return {};
	}

	return std::unique_ptr<
		void, LoadableLibraryManager::SharedLibraryDeleter>(handle);
}

} // namespace

LoadableLibraryManager::~LoadableLibraryManager() = default;

void LoadableLibraryManager::SharedLibraryDeleter::operator()(void* handle) const
{
	if (handle) {
		dlclose(handle);
	}
}

void LoadableLibraryManager::appendLoadedSharedLibrary(
	const std::shared_ptr<LoadedSharedLibrary>& loadedLibrary)
{
	s.rsrc.loadedSharedLibraries.push_back(loadedLibrary);
}

void LoadableLibraryManager::removeLoadedSharedLibrary(
	const std::shared_ptr<LoadedSharedLibrary>& loadedLibrary)
{
	auto& loadedLibs = s.rsrc.loadedSharedLibraries;
	auto it = std::find(loadedLibs.begin(), loadedLibs.end(), loadedLibrary);
	if (it != loadedLibs.end()) {
		loadedLibs.erase(it);
	}
}

std::optional<std::string> LoadableLibraryManager::searchForLibInSmoSearchPaths(
	const std::string& libraryPath) const
{
	const std::vector<std::string> searchPaths = buildSmoLibrarySearchPaths();

	for (const auto& path : searchPaths)
	{
		fs::path fullPath = fs::path(path) / libraryPath;
		if (fs::exists(fullPath)) {
			return fullPath.string();
		}
	}

	logLibraryNotInSearchPaths(libraryPath, searchPaths);
	return std::nullopt;
}

std::shared_ptr<LoadableLibraryManager::LoadedSharedLibrary>
LoadableLibraryManager::loadSharedLibrary(const std::string& libraryPath)
{
	const std::optional<std::string> fullPath =
		searchForLibInSmoSearchPaths(libraryPath);
	const std::string resolvedPath = fullPath.value_or(libraryPath);

	auto dlopenHandle = tryDlopenPath(resolvedPath);
	if (!dlopenHandle && fullPath.has_value()) {
		dlopenHandle = tryDlopenPath(libraryPath);
	}

	if (!dlopenHandle)
	{
		const char* dlerr = dlerror();
		const std::string error = (dlerr
			? dlerr
			: "Unknown error while opening shlib");
		throw std::runtime_error(
			std::string("loadSharedLibrary: Cannot load library '")
			+ libraryPath + "': " + error);
	}

	auto loadedLibrary = std::make_shared<LoadedSharedLibrary>(
		libraryPath,
		resolvedPath,
		std::move(dlopenHandle));

	appendLoadedSharedLibrary(loadedLibrary);
	return loadedLibrary;
}

void LoadableLibraryManager::unloadSharedLibrary(
	const std::shared_ptr<LoadedSharedLibrary>& loadedLibrary)
{
	if (!loadedLibrary) {
		return;
	}

	removeLoadedSharedLibrary(loadedLibrary);
	loadedLibrary->dlopenHandle.reset();
}

} // namespace loadable_lib
} // namespace smo
