/* DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
 *                   Version 2, December 2004
 *
 * Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
 *
 * Everyone is permitted to copy and distribute verbatim or modified
 * copies of this license document, and changing it is allowed as long
 * as the name is changed.
 *
 *            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
 *   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
 *
 *  0. You just DO WHAT THE FUCK YOU WANT TO.
 */

#include "llviewerprecompiledheaders.h"

#include "NACLantispam.h"
#include "llagent.h"
#include "llavataractions.h"
#include "llcallbacklist.h" // For idle cleaning
#include "llnotificationsutil.h"
#include "lltrans.h"
#include "llviewerobjectlist.h"

#include <time.h>

class NACLAntiSpamQueue
{
	friend class NACLAntiSpamRegistry;
public:
	const U32& getAmount() const { return mAmount; }
	const U32& getTime() const { return mTime; }
protected:
	NACLAntiSpamQueue(const U32& time, const U32& amount) : mTime(time), mAmount(amount) {}
	void setAmount(const U32& amount) { mAmount = amount; }
	void setTime(const U32& time) { mTime = time; }
	void block(const LLUUID& source) { mEntries[source.asString()].block(); }
	void reset() { mEntries.clear(); }
	// Returns 0 if unblocked/disabled, 1 if check results in a new block, 2 if by an existing block
	U8 check(const LLUUID& source, const U32& multiplier)
	{
		const auto key = source.asString();
		auto it = mEntries.find(key);
		if (it != mEntries.end())
			return it->second.blockIfNeeded(mAmount * multiplier, mTime);
		mEntries[key]; // Default construct an Entry
		return 0U;
	}
	void idle()
	{
		// Clean out old unblocked entries
		const auto time_limit = mTime + 1; // One second after time has gone up, the next offense would reset anyway
		for (auto it = mEntries.begin(); it != mEntries.end();)
		{
			const auto& entry = it->second;
			if (entry.getBlocked() || entry.withinBlockTime(time_limit))
				++it;
			else it = mEntries.erase(it);
		}
	}

private:
	class Entry
	{
		friend class NACLAntiSpamQueue;
	protected:
		void reset() { updateTime(); mAmount = 1; mBlocked = false; }
		const U32& getAmount() const { return mAmount; }
		const std::time_t& getTime() const { return mTime; }
		void updateTime() { mTime = time(nullptr); }
		void block() { mBlocked = true; }
		bool withinBlockTime(const U32& time_limit) const { return (time(nullptr) - mTime) <= time_limit; }
		U8 blockIfNeeded(const U32& amount, const U32& time_limit)
		{
			if (mBlocked) return 2U; // Already blocked
			if (withinBlockTime(time_limit))
			{
				if (++mAmount > amount)
				{
					block();
					return 1U;
				}
			}
			else reset(); // Enough time has passed to forgive or not already in list
			return 0U;
		}
		bool getBlocked() const { return mBlocked; }
	private:
		U32 mAmount = 1;
		std::time_t mTime = time(nullptr);
		bool mBlocked = false;
	};
	boost::unordered_map<std::string, Entry> mEntries;
	U32 mAmount, mTime;
};

bool can_block(const LLUUID& id)
{
	if (id.isNull() || gAgentID == id) return false; // Can't block system or self.
	if (const LLViewerObject* obj = gObjectList.findObject(id)) // From an object,
		return !obj->permYouOwner(); // not own object.
	return true;
}

bool is_collision_sound(const std::string& sound)
{
	// The following sounds will be ignored for purposes of spam protection. They have been gathered from wiki documentation of frequent official sounds.
	const std::array<const std::string, 29> COLLISION_SOUNDS = {
		"dce5fdd4-afe4-4ea1-822f-dd52cac46b08",
		"51011582-fbca-4580-ae9e-1a5593f094ec",
		"68d62208-e257-4d0c-bbe2-20c9ea9760bb",
		"75872e8c-bc39-451b-9b0b-042d7ba36cba",
		"6a45ba0b-5775-4ea8-8513-26008a17f873",
		"992a6d1b-8c77-40e0-9495-4098ce539694",
		"2de4da5a-faf8-46be-bac6-c4d74f1e5767",
		"6e3fb0f7-6d9c-42ca-b86b-1122ff562d7d",
		"14209133-4961-4acc-9649-53fc38ee1667",
		"bc4a4348-cfcc-4e5e-908e-8a52a8915fe6",
		"9e5c1297-6eed-40c0-825a-d9bcd86e3193",
		"e534761c-1894-4b61-b20c-658a6fb68157",
		"8761f73f-6cf9-4186-8aaa-0948ed002db1",
		"874a26fd-142f-4173-8c5b-890cd846c74d",
		"0e24a717-b97e-4b77-9c94-b59a5a88b2da",
		"75cf3ade-9a5b-4c4d-bb35-f9799bda7fb2",
		"153c8bf7-fb89-4d89-b263-47e58b1b4774",
		"55c3e0ce-275a-46fa-82ff-e0465f5e8703",
		"24babf58-7156-4841-9a3f-761bdbb8e237",
		"aca261d8-e145-4610-9e20-9eff990f2c12",
		"0642fba6-5dcf-4d62-8e7b-94dbb529d117",
		"25a863e8-dc42-4e8a-a357-e76422ace9b5",
		"9538f37c-456e-4047-81be-6435045608d4",
		"8c0f84c3-9afd-4396-b5f5-9bca2c911c20",
		"be582e5d-b123-41a2-a150-454c39e961c8",
		"c70141d4-ba06-41ea-bcbc-35ea81cb8335",
		"7d1826f4-24c4-4aac-8c2e-eff45df37783",
		"063c97d3-033a-4e9b-98d8-05c8074922cb",
		"00000000-0000-0000-0000-000000000120"
	};

	for (const auto& collision : COLLISION_SOUNDS)
		if (collision == sound)
			return true;
	return false;
}
// NaClAntiSpamRegistry

constexpr std::array<const char*, NACLAntiSpamRegistry::QUEUE_MAX> QUEUE_NAME = {
	"Chat",
	"Inventory",
	"Instant Message",
	"calling card",
	"sound",
	"Sound Preload",
	"Script Dialog",
	"Teleport"
};

NACLAntiSpamRegistry::NACLAntiSpamRegistry()
{
	auto control = gSavedSettings.getControl("_NACL_AntiSpamTime");
	const U32 time = control->get().asInteger();
	mConnections[0] = control->getSignal()->connect(boost::bind(&NACLAntiSpamRegistry::handleNaclAntiSpamTimeChanged, _2));

	control = gSavedSettings.getControl("_NACL_AntiSpamAmount");
	const U32 amount = control->get().asInteger();
	mConnections[1] = control->getSignal()->connect(boost::bind(&NACLAntiSpamRegistry::handleNaclAntiSpamAmountChanged, _2));

	control = gSavedSettings.getControl("_NACL_AntiSpamGlobalQueue");
	mConnections[2] = control->getSignal()->connect(boost::bind(&NACLAntiSpamRegistry::handleNaclAntiSpamGlobalQueueChanged, _2));
	initializeQueues(control->get(), time, amount);
}

void NACLAntiSpamRegistry::initializeQueues(bool global, const U32& time, const U32& amount)
{
	if (global) // If Global, initialize global queue
		mGlobalQueue.reset(new NACLAntiSpamQueue(time, amount));
	else
	{
		mQueues.reset(new std::array<NACLAntiSpamQueue, QUEUE_MAX>{{
			NACLAntiSpamQueue(time, amount), // QUEUE_CHAT
			NACLAntiSpamQueue(time, amount), // QUEUE_INVENTORY
			NACLAntiSpamQueue(time, amount), // QUEUE_IM
			NACLAntiSpamQueue(time, amount), // QUEUE_CALLING_CARD
			NACLAntiSpamQueue(time, amount), // QUEUE_SOUND
			NACLAntiSpamQueue(time, amount), // QUEUE_SOUND_PRELOAD
			NACLAntiSpamQueue(time, amount), // QUEUE_SCRIPT_DIALOG
			NACLAntiSpamQueue(time, amount)  // QUEUE_TELEPORT
		}});
	}
}

constexpr const char* getQueueName(const NACLAntiSpamRegistry::Type& name)
{
	return name >= QUEUE_NAME.size() ? "Unknown" : QUEUE_NAME[name];
}

void NACLAntiSpamRegistry::setAllQueueTimes(U32 time)
{
	if (mGlobalQueue) mGlobalQueue->setTime(time);
	else for(auto& queue : *mQueues) queue.setTime(time);
}

void NACLAntiSpamRegistry::setAllQueueAmounts(U32 amount)
{
	if (mGlobalQueue) mGlobalQueue->setAmount(amount);
	else for (U8 queue = 0U; queue < QUEUE_MAX; ++queue)
	{
		auto& q = (*mQueues)[queue];
		if (queue == QUEUE_SOUND || queue == QUEUE_SOUND_PRELOAD)
			q.setAmount(amount*5);
		else
			q.setAmount(amount);
	}
}

void NACLAntiSpamRegistry::blockOnQueue(const Type& name, const LLUUID& source)
{
	if (name >= QUEUE_MAX)
		LL_ERRS("AntiSpam") << "CODE BUG: Attempting to use a antispam queue that was outside of the reasonable range of queues. Queue: " << getQueueName(name) << LL_ENDL;
	else (mGlobalQueue ? *mGlobalQueue : (*mQueues)[name]).block(source);
}

bool NACLAntiSpamRegistry::checkQueue(const Type& name, const LLUUID& source, const LFIDBearer::Type& type, const U32& multiplier)
//returns true if blocked
{
	if (name >= QUEUE_MAX)
	{
		LL_ERRS("AntiSpam") << "CODE BUG: Attempting to check antispam queue that was outside of the reasonable range of queues. Queue: " << getQueueName(name) << LL_ENDL;
		return false;
	}

	if (!can_block(source)) return false;

	auto& queue = mGlobalQueue ? *mGlobalQueue : (*mQueues)[name];
	const auto result = queue.check(source, multiplier);
	if (!result) return false; // Safe

	if (result != 2 // Not previously blocked
	&& gSavedSettings.getBOOL("AntiSpamNotify")) // and Just blocked!
	{
		const std::string get_slurl_for(const LLUUID& id, const LFIDBearer::Type& type);
		const auto slurl = get_slurl_for(source, type);
		LLSD args;
		args["SOURCE"] = slurl.empty() ? source.asString() : slurl;
		args["TYPE"] = LLTrans::getString(getQueueName(name));
		args["AMOUNT"] = (LLSD::Integer)(multiplier * queue.getAmount());
		args["TIME"] = (LLSD::Integer)queue.getTime();
		LLNotificationsUtil::add("AntiSpamBlock", args);
	}
	return true;
}

void NACLAntiSpamRegistry::idle()
{
	if (mGlobalQueue) mGlobalQueue->idle();
	else for (auto& queue : *mQueues) queue.idle();
}

void NACLAntiSpamRegistry::resetQueues()
{
	if (mGlobalQueue) mGlobalQueue->reset();
	else for (auto& queue : *mQueues) queue.reset();

	LL_INFOS() << "AntiSpam Queues Reset" << LL_ENDL;
}

void NACLAntiSpamRegistry::purgeAllQueues()
{
	// Note: These resets are upon the unique_ptr, not the Queue itself!
	if (mGlobalQueue)
		mGlobalQueue.reset();
	else
		mQueues.reset();
}

// Handlers
// static
void NACLAntiSpamRegistry::startup()
{
	auto onAntiSpamToggle = [](const LLControlVariable*, const LLSD& value) {
		if (value.asBoolean()) instance();
		else deleteSingleton();
	};
	auto control = gSavedSettings.getControl("AntiSpamEnabled");
	control->getSignal()->connect(onAntiSpamToggle);
	onAntiSpamToggle(control, control->get());
}

// static
bool NACLAntiSpamRegistry::handleNaclAntiSpamGlobalQueueChanged(const LLSD& newvalue)
{
	if (instanceExists())
	{
		auto& inst = instance();
		inst.purgeAllQueues();
		inst.initializeQueues(newvalue.asBoolean(), gSavedSettings.getU32("_NACL_AntiSpamTime"), gSavedSettings.getU32("_NACL_AntiSpamAmount"));
	}
    return true;
}
//static
bool NACLAntiSpamRegistry::handleNaclAntiSpamTimeChanged(const LLSD& newvalue)
{
	if (auto inst = getIfExists()) inst->setAllQueueTimes(newvalue.asInteger());
    return true;
}
//static
bool NACLAntiSpamRegistry::handleNaclAntiSpamAmountChanged(const LLSD& newvalue)
{
	if (auto inst = getIfExists()) inst->setAllQueueAmounts(newvalue.asInteger());
    return true;
}