/*
 * InspIRCd -- Internet Relay Chat Daemon
 *
 *   Copyright (C) 2020 Sadie Powell <sadie@witchery.services>
 *
 * This file is part of InspIRCd.  InspIRCd is free software: you can
 * redistribute it and/or modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation, version 2.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */


#include "inspircd.h"

typedef std::bitset<UCHAR_MAX + 1> AllowedChars;

namespace
{
	// The characters which are allowed in nicknames.
	AllowedChars allowedchars;

	// The characters which are allowed at the front of a nickname.
	AllowedChars allowedfrontchars;

	// The mapping of lower case characters to upper case characters.
	unsigned char casemap[UCHAR_MAX];

	bool IsValidNick(const std::string& nick)
	{
		if (nick.empty() || nick.length() > ServerInstance->Config->Limits.NickMax)
			return false;

		for (std::string::const_iterator iter = nick.begin(); iter != nick.end(); ++iter)
		{
			unsigned char chr = static_cast<unsigned char>(*iter);

			// Check that the character is allowed at the front of the nick.
			if (iter == nick.begin() && !allowedfrontchars[chr])
				return false;

			// Check that the character is allowed in the nick.
			if (!allowedchars[chr])
				return false;
		}

		return true;
	}
}

class ModuleCodepage
	: public Module
{
 private:
	// The character map which was set before this module was loaded.
	const unsigned char* origcasemap;

	// The name of the character map which was set before this module was loaded.
	const std::string origcasemapname;

	// The IsNick handler which was set before this module was loaded.
	const TR1NS::function<bool(const std::string&)> origisnick;

	template <typename T>
	void RehashHashmap(T& hashmap)
	{
		T newhash(hashmap.bucket_count());
		for (typename T::const_iterator i = hashmap.begin(); i != hashmap.end(); ++i)
			newhash.insert(std::make_pair(i->first, i->second));
		hashmap.swap(newhash);
	}

	void CheckDuplicateNick()
	{
		insp::flat_set<std::string, irc::insensitive_swo> duplicates;
		const UserManager::LocalList& list = ServerInstance->Users.GetLocalUsers();
		for (UserManager::LocalList::const_iterator iter = list.begin(); iter != list.end(); ++iter)
		{
			LocalUser* user = *iter;
			if (user->nick != user->uuid && !duplicates.insert(user->nick).second)
			{
				user->WriteNumeric(RPL_SAVENICK, user->uuid, "Your nickname is no longer available.");
				user->ChangeNick(user->uuid);
			}
		}
	}

	void CheckInvalidNick()
	{
		const UserManager::LocalList& list = ServerInstance->Users.GetLocalUsers();
		for (UserManager::LocalList::const_iterator iter = list.begin(); iter != list.end(); ++iter)
		{
			LocalUser* user = *iter;
			if (user->nick != user->uuid && !ServerInstance->IsNick(user->nick))
			{
				user->WriteNumeric(RPL_SAVENICK, user->uuid, "Your nickname is no longer valid.");
				user->ChangeNick(user->uuid);
			}
		}
	}

	void CheckRehash(unsigned char* prevmap)
	{
		if (!memcmp(prevmap, national_case_insensitive_map, UCHAR_MAX))
			return;

		RehashHashmap(ServerInstance->Users.clientlist);
		RehashHashmap(ServerInstance->Users.uuidlist);
		RehashHashmap(ServerInstance->chanlist);
	}

 public:
	ModuleCodepage()
		: origcasemap(national_case_insensitive_map)
		, origcasemapname(ServerInstance->Config->CaseMapping)
		, origisnick(ServerInstance->IsNick)
	{
	}

	~ModuleCodepage()
	{
		ServerInstance->IsNick = origisnick;
		CheckInvalidNick();

		ServerInstance->Config->CaseMapping = origcasemapname;
		national_case_insensitive_map = origcasemap;
		CheckDuplicateNick();
		CheckRehash(casemap);

		ServerInstance->ISupport.Build();
	}

	void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE
	{
		const std::string name = ServerInstance->Config->ConfValue("codepage")->getString("name");
		if (name.empty())
			throw ModuleException("<codepage:name> is a required field!");

		AllowedChars newallowedchars;
		AllowedChars newallowedfrontchars;
		ConfigTagList cpchars = ServerInstance->Config->ConfTags("cpchars");
		for (ConfigIter i = cpchars.first; i != cpchars.second; ++i)
		{
			ConfigTag* tag = i->second;

			unsigned char begin = tag->getUInt("begin", tag->getUInt("index", 0), 1, UCHAR_MAX);
			if (!begin)
				throw ModuleException("<cpchars> tag without index or begin specified at " + tag->getTagLocation());

			unsigned char end = tag->getUInt("end", begin, 1, UCHAR_MAX);
			if (begin > end)
				throw ModuleException("<cpchars:begin> must be lower than <cpchars:end> at " + tag->getTagLocation());

			bool front = tag->getBool("front", false);
			for (unsigned short pos = begin; pos <= end; ++pos)
			{
				if (pos == '\n' || pos == '\r' || pos == ' ')
				{
					throw ModuleException(InspIRCd::Format("<cpchars> tag contains a forbidden character: %u at %s",
						pos, tag->getTagLocation().c_str()));
				}

				if (front && (pos == ':' || isdigit(pos)))
				{
					throw ModuleException(InspIRCd::Format("<cpchars> tag contains a forbidden front character: %u at %s",
						pos, tag->getTagLocation().c_str()));
				}

				newallowedchars.set(pos);
				newallowedfrontchars.set(pos, front);
				ServerInstance->Logs->Log(MODNAME, LOG_DEBUG, "Marked %u (%c) as allowed (front: %s)",
					pos, pos, front ? "yes" : "no");
			}
		}

		unsigned char newcasemap[UCHAR_MAX];
		for (size_t i = 0; i < UCHAR_MAX; ++i)
			newcasemap[i] = i;
		ConfigTagList cpcase = ServerInstance->Config->ConfTags("cpcase");
		for (ConfigIter i = cpcase.first; i != cpcase.second; ++i)
		{
			ConfigTag* tag = i->second;

			unsigned char lower = tag->getUInt("lower", 0, 1, UCHAR_MAX);
			if (!lower)
				throw ModuleException("<cpcase:lower> is required at " + tag->getTagLocation());

			unsigned char upper = tag->getUInt("upper", 0, 1, UCHAR_MAX);
			if (!upper)
				throw ModuleException("<cpcase:upper> is required at " + tag->getTagLocation());

			newcasemap[upper] = lower;
			ServerInstance->Logs->Log(MODNAME, LOG_DEBUG, "Marked %u (%c) as the lower case version of %u (%c)",
				lower, lower, upper, upper);
		}

		std::swap(allowedchars, newallowedchars);
		std::swap(allowedfrontchars, newallowedfrontchars);
		std::swap(casemap, newcasemap);

		ServerInstance->IsNick = &IsValidNick;
		CheckInvalidNick();

		ServerInstance->Config->CaseMapping = name;
		national_case_insensitive_map = casemap;
		CheckRehash(newcasemap);

		ServerInstance->ISupport.Build();
	}

	Version GetVersion() CXX11_OVERRIDE
	{
		std::stringstream linkdata;

		linkdata << "front=";
		for (size_t i = 0; i < allowedfrontchars.size(); ++i)
			if (allowedfrontchars[i])
				linkdata << static_cast<unsigned char>(i);

		linkdata << "&middle=";
		for (size_t i = 0; i < allowedchars.size(); ++i)
			if (allowedchars[i])
				linkdata << static_cast<unsigned char>(i);

		linkdata << "&map=";
		for (size_t i = 0; i < sizeof(casemap); ++i)
			if (casemap[i] != i)
				linkdata << static_cast<unsigned char>(i) << casemap[i] << ',';

		return Version("Allows the server administrator to define what characters are allowed in nicknames and how characters should be compared in a case insensitive way.", VF_COMMON | VF_VENDOR, linkdata.str());
	}
};
MODULE_INIT(ModuleCodepage)