// Anope IRC Services <https://www.anope.org/>
//
// Copyright (C) 2003-2026 Anope Contributors
//
// Anope is free software. You can use, modify, and/or distribute it under the
// terms of version 2 of the GNU General Public License. See docs/LICENSE.txt
// for the complete terms of this license and docs/AUTHORS.txt for a list of
// contributors.
//
// Based on the original code of Epona by Lara
// Based on the original code of Services by Andy Church
//
// SPDX-License-Identifier: GPL-2.0-only

#include "services.h"
#include "modules.h"
#include "users.h"
#include "regchannel.h"
#include "config.h"

#include <sys/types.h>
#include <sys/stat.h>
#ifndef _WIN32
#include <sys/types.h>
#include <dlfcn.h>
#endif

#include <filesystem>
namespace fs = std::filesystem;

std::list<Module *> ModuleManager::Modules;
std::vector<Module *> ModuleManager::EventHandlers[I_SIZE];

#ifdef _WIN32
void ModuleManager::CleanupRuntimeDirectory()
{
	Anope::string dirbuf = Anope::ExpandData("runtime");

	Log(LOG_DEBUG) << "Cleaning out Module run time directory (" << dirbuf << ") - this may take a moment, please wait";
	try
	{
		for (const auto &entry : fs::directory_iterator(dirbuf.str()))
		{
			if (entry.is_regular_file())
				fs::remove(entry);
		}
	}
	catch (const fs::filesystem_error &err)
	{
		Log(LOG_DEBUG) << "Cannot open directory (" << dirbuf << "): " << err.what();
	}
}
#endif

/* This code was found online at https://web.archive.org/web/20180318184211/https://www.linuxjournal.com/article/3687#comment-26593
 *
 * This function will take a pointer from either dlsym or GetProcAddress and cast it in
 * a way that won't cause C++ warnings/errors to come up.
 */
template <class TYPE> static TYPE function_cast(void *symbol)
{
	union
	{
		void *symbol;
		TYPE function;
	} cast;
	cast.symbol = symbol;
	return cast.function;
}

ModuleReturn ModuleManager::LoadModule(const Anope::string &modname, User *u)
{
	if (modname.empty())
		return MOD_ERR_PARAMS;

	if (FindModule(modname))
		return MOD_ERR_EXISTS;

	Log(LOG_DEBUG) << "Trying to load module: " << modname;

	const auto modpath = Anope::ExpandModule(modname + DLL_EXT);
	std::error_code ec;
	if (!fs::exists(modpath.str()) || ec)
	{
		Log(LOG_TERMINAL) << "Error while loading " << modname << ": file does not exist";
		return MOD_ERR_NOEXIST;
	}

#ifdef _WIN32
	/* Generate the filename for the temporary copy of the module */
	auto pbuf = Anope::ExpandData("runtime/" + modname + DLL_EXT ".XXXXXX");
	if (!fs::copy_file(modpath.str(), pbuf.str(), fs::copy_options::overwrite_existing, ec) || ec)
	{
		Log(LOG_TERMINAL) << "Error while copying " << modname << " from " << modpath
			<< " to " << pbuf << ": " << ec.message();
		return MOD_ERR_FILE_IO;
	}
#else
	const auto pbuf = modpath;
#endif

	dlerror();
	void *handle = dlopen(pbuf.c_str(), RTLD_NOW);
	const char *err = dlerror();
	if (!handle)
	{
		if (err && *err)
			Log() << err;
		return MOD_ERR_NOLOAD;
	}

	try
	{
		ModuleVersion v = GetVersion(handle);

		if (v.GetMajor() < Anope::VersionMajor() || (v.GetMajor() == Anope::VersionMajor() && v.GetMinor() < Anope::VersionMinor()))
		{
			Log() << "Module " << modname << " is compiled against an older version of Anope " << v.GetMajor() << "." << v.GetMinor() << ", this is " << Anope::VersionShort();
			dlclose(handle);
			return MOD_ERR_VERSION;
		}
		else if (v.GetMajor() > Anope::VersionMajor() || (v.GetMajor() == Anope::VersionMajor() && v.GetMinor() > Anope::VersionMinor()))
		{
			Log() << "Module " << modname << " is compiled against a newer version of Anope " << v.GetMajor() << "." << v.GetMinor() << ", this is " << Anope::VersionShort();
			dlclose(handle);
			return MOD_ERR_VERSION;
		}
		else if (v.GetPatch() < Anope::VersionPatch())
		{
			Log() << "Module " << modname << " is compiled against an older version of Anope, " << v.GetMajor() << "." << v.GetMinor() << "." << v.GetPatch() << ", this is " << Anope::VersionShort();
			dlclose(handle);
			return MOD_ERR_VERSION;
		}
		else if (v.GetPatch() > Anope::VersionPatch())
		{
			Log() << "Module " << modname << " is compiled against a newer version of Anope, " << v.GetMajor() << "." << v.GetMinor() << "." << v.GetPatch() << ", this is " << Anope::VersionShort();
			dlclose(handle);
			return MOD_ERR_VERSION;
		}
		else
		{
			Log(LOG_DEBUG_2) << "Module " << modname << " is compiled against current version of Anope " << Anope::VersionShort();
		}
	}
	catch (const ModuleException &ex)
	{
		/* this error has already been logged */
		dlclose(handle);
		return MOD_ERR_NOLOAD;
	}

	dlerror();
	auto *func = function_cast<Module *(*)(const Anope::string &, const Anope::string &)>(dlsym(handle, "AnopeInit"));
	err = dlerror();
	if (!func)
	{
		Log() << "No init function found, not an Anope module";
		if (err && *err)
			Log(LOG_DEBUG) << err;
		dlclose(handle);
		return MOD_ERR_NOLOAD;
	}

	/* Create module. */
	Anope::string nick;
	if (u)
		nick = u->nick;

	Module *m;

	ModuleReturn moderr = MOD_ERR_OK;
	try
	{
		m = func(modname, nick);
	}
	catch (const ModuleException &ex)
	{
		Log() << "Error while loading " << modname << ": " << ex.GetReason();
		moderr = MOD_ERR_EXCEPTION;
	}

	if (moderr != MOD_ERR_OK)
	{
		if (dlclose(handle))
			Log() << dlerror();
		return moderr;
	}

	m->filename = pbuf;
	m->handle = handle;

	if (m->type & DEPRECATED)
		Log(LOG_TERMINAL) << "Warning: " << modname << " is deprecated and will be removed in a future release";

	/* Initialize config */
	try
	{
		m->OnReload(*Config);
	}
	catch (const ModuleException &ex)
	{
		Log() << "Module " << modname << " couldn't load:" << ex.GetReason();
		moderr = MOD_ERR_EXCEPTION;
	}
	catch (const ConfigException &ex)
	{
		Log() << "Module " << modname << " couldn't load due to configuration problems: " << ex.GetReason();
		moderr = MOD_ERR_EXCEPTION;
	}
	catch (const NotImplementedException &ex)
	{
	}

	if (moderr != MOD_ERR_OK)
	{
		DeleteModule(m);
		return moderr;
	}

	Log(LOG_DEBUG) << "Module " << modname << " loaded.";

	/* Attach module to all events */
	for (auto &mods : EventHandlers)
		mods.push_back(m);

	Serialize::CreateTypes();
	m->Prioritize();

	FOREACH_MOD(OnModuleLoad, (u, m));

	return MOD_ERR_OK;
}

ModuleVersion ModuleManager::GetVersion(void *handle)
{
	dlerror();
	auto *func = function_cast<ModuleVersionC (*)()>(dlsym(handle, "AnopeVersion"));;
	if (!func)
	{
		Log() << "No version function found, not an Anope module";

		const char *err = dlerror();
		if (err && *err)
			Log(LOG_DEBUG) << err;

		throw ModuleException("No version");
	}

	return func();
}

ModuleReturn ModuleManager::UnloadModule(Module *m, User *u)
{
	if (!m)
		return MOD_ERR_PARAMS;

	FOREACH_MOD(OnModuleUnload, (u, m));

	return DeleteModule(m);
}

Module *ModuleManager::FindModule(const Anope::string &name)
{
	for (auto *m : Modules)
	{
		if (m->name.equals_ci(name))
			return m;
	}

	return NULL;
}

Module *ModuleManager::FindFirstOf(ModType type, bool ignoredeprecated)
{
	for (auto *m : Modules)
	{
		if (ignoredeprecated && (m->type & DEPRECATED))
			continue;

		if (m->type & type)
			return m;
	}

	return NULL;
}

void ModuleManager::RequireVersion(unsigned major, unsigned minor, unsigned patch)
{
	if (Anope::VersionMajor() > major)
		return;
	else if (Anope::VersionMajor() == major)
	{
		if (Anope::VersionMinor() > minor)
			return;
		else if (Anope::VersionMinor() == minor)
		{
			if (Anope::VersionPatch() > patch)
				return;
			else if (Anope::VersionPatch() == patch)
				return;
		}
	}

	throw ModuleException("This module requires version " + Anope::ToString(major) + "." + Anope::ToString(minor) + "." + Anope::ToString(patch) + " - this is " + Anope::VersionShort());
}

ModuleReturn ModuleManager::DeleteModule(Module *m)
{
	if (!m || !m->handle)
		return MOD_ERR_PARAMS;

	void *handle = m->handle;
	Anope::string filename = m->filename;

	Log(LOG_DEBUG) << "Unloading module " << m->name;

	dlerror();
	auto *destroy_func = function_cast<void (*)(Module *)>(dlsym(m->handle, "AnopeFini"));
	const char *err = dlerror();
	if (!destroy_func || (err && *err))
	{
		Log() << "No destroy function found for " << m->name << ", chancing delete...";
		delete m; /* we just have to chance they haven't overwrote the delete operator then... */
	}
	else
		destroy_func(m); /* Let the module delete it self, just in case */

	if (dlclose(handle))
		Log() << dlerror();

#ifdef _WIN32
	if (!filename.empty())
		unlink(filename.c_str());
#endif

	return MOD_ERR_OK;
}

void ModuleManager::DetachAll(Module *mod)
{
	for (auto &mods : EventHandlers)
	{
		auto it2 = std::find(mods.begin(), mods.end(), mod);
		if (it2 != mods.end())
			mods.erase(it2);
	}
}

bool ModuleManager::SetPriority(Module *mod, Priority s)
{
	for (unsigned i = 0; i < I_SIZE; ++i)
		SetPriority(mod, static_cast<Implementation>(i), s);

	return true;
}

bool ModuleManager::SetPriority(Module *mod, Implementation i, Priority s, Module **modules, size_t sz)
{
	/** To change the priority of a module, we first find its position in the vector,
	 * then we find the position of the other modules in the vector that this module
	 * wants to be before/after. We pick off either the first or last of these depending
	 * on which they want, and we make sure our module is *at least* before or after
	 * the first or last of this subset, depending again on the type of priority.
	 */

	/* Locate our module. This is O(n) but it only occurs on module load so we're
	 * not too bothered about it
	 */
	size_t source = 0;
	bool found = false;
	for (size_t x = 0, end = EventHandlers[i].size(); x != end; ++x)
		if (EventHandlers[i][x] == mod)
		{
			source = x;
			found = true;
			break;
		}

	/* Eh? this module doesn't exist, probably trying to set priority on an event
	 * they're not attached to.
	 */
	if (!found)
		return false;

	size_t swap_pos = 0;
	bool swap = true;
	switch (s)
	{
		/* Dummy value */
		case PRIORITY_DONTCARE:
		{
			swap = false;
			break;
		}
		/* Module wants to be first, sod everything else */
		case PRIORITY_FIRST:
		{
			swap_pos = 0;
			break;
		}
		/* Module is submissive and wants to be last... awww. */
		case PRIORITY_LAST:
		{
			if (EventHandlers[i].empty())
				swap_pos = 0;
			else
				swap_pos = EventHandlers[i].size() - 1;
			break;
		}
		/* Place this module after a set of other modules */
		case PRIORITY_AFTER:
		{
			/* Find the latest possible position */
			swap_pos = 0;
			swap = false;
			for (size_t x = 0, end = EventHandlers[i].size(); x != end; ++x)
			{
				for (size_t n = 0; n < sz; ++n)
				{
					if (modules[n] && EventHandlers[i][x] == modules[n] && x >= swap_pos && source <= swap_pos)
					{
						swap_pos = x;
						swap = true;
					}
				}
			}
			break;
		}
		/* Place this module before a set of other modules */
		case PRIORITY_BEFORE:
		{
			swap_pos = EventHandlers[i].size() - 1;
			swap = false;
			for (size_t x = 0, end = EventHandlers[i].size(); x != end; ++x)
			{
				for (size_t n = 0; n < sz; ++n)
				{
					if (modules[n] && EventHandlers[i][x] == modules[n] && x <= swap_pos && source >= swap_pos)
					{
						swap = true;
						swap_pos = x;
					}
				}
			}
			break;
		}
	}

	/* Do we need to swap? */
	if (swap && swap_pos != source)
	{
		/* Suggestion from Phoenix, "shuffle" the modules to better retain call order */
		int increment = 1;

		if (source > swap_pos)
			increment = -1;

		for (unsigned j = source; j != swap_pos; j += increment)
		{
			if (j + increment > EventHandlers[i].size() - 1 || (!j && increment == -1))
				continue;

			std::swap(EventHandlers[i][j], EventHandlers[i][j + increment]);
		}
	}

	return true;
}

void ModuleManager::UnloadAll()
{
	std::vector<Anope::string> modules;
	for (size_t i = 1, j = 0; i != MT_END; j |= i, i <<= 1)
	{
		for (auto *m : Modules)
		{
			if ((m->type & j) == m->type)
				modules.push_back(m->name);
		}
	}

	for (auto &module : modules)
	{
		Module *m = FindModule(module);
		if (m != NULL)
			UnloadModule(m, NULL);
	}
}
