/** 
 * @file llmutelist.cpp
 * @author Richard Nelson, James Cook
 * @brief Management of list of muted players
 *
 * Copyright (c) 2003-$CurrentYear$, Linden Research, Inc.
 * $License$
 */

/*
 * How should muting work?
 * Mute an avatar
 * Mute a specific object (accidentally spamming)
 *
 * right-click avatar, mute
 * see list of recent chatters, mute
 * type a name to mute?
 *
 * show in list whether chatter is avatar or object
 *
 * need fast lookup by id
 * need lookup by name, doesn't have to be fast
 */

#include "llviewerprecompiledheaders.h"

#include "llmutelist.h"

#include <boost/tokenizer.hpp>

#include "llcrc.h"
#include "lldispatcher.h"
#include "llxfermanager.h"
#include "message.h"
#include "lldir.h"

#include "llagent.h"
#include "llfloatermute.h"
#include "llviewermessage.h"	// for gGenericDispatcher
#include "llviewerwindow.h"
#include "viewer.h"
#include "llworld.h" //for particle system banning

LLMuteList* gMuteListp = NULL;

// "emptymutelist"
class LLDispatchEmptyMuteList : public LLDispatchHandler
{
public:
	virtual bool operator()(
		const LLDispatcher* dispatcher,
		const std::string& key,
		const LLUUID& invoice,
		const sparam_t& strings)
	{
		gMuteListp->setLoaded();
		return true;
	}
};

static LLDispatchEmptyMuteList sDispatchEmptyMuteList;

//-----------------------------------------------------------------------------
// LLMute()
//-----------------------------------------------------------------------------
const char BY_NAME_SUFFIX[] = " (by name)";
const char AGENT_SUFFIX[] = " (resident)";
const char OBJECT_SUFFIX[] = " (object)";
const char GROUP_SUFFIX[] = " (group)";

LLString LLMute::getDisplayName() const
{
	LLString name_with_suffix = mName;
	switch (mType)
	{
		case BY_NAME:
		default:
			name_with_suffix += BY_NAME_SUFFIX;
			break;
		case AGENT:
			name_with_suffix += AGENT_SUFFIX;
			break;
		case OBJECT:
			name_with_suffix += OBJECT_SUFFIX;
			break;
		case GROUP:
			name_with_suffix += GROUP_SUFFIX;
			break;
	}
	return name_with_suffix;
}

void LLMute::setFromDisplayName(const LLString& display_name)
{
	size_t pos = 0;
	mName = display_name;
	
	pos = mName.rfind(GROUP_SUFFIX);
	if (pos != std::string::npos)
	{
		mName.erase(pos);
		mType = GROUP;
		return;
	}
	
	pos = mName.rfind(OBJECT_SUFFIX);
	if (pos != std::string::npos)
	{
		mName.erase(pos);
		mType = OBJECT;
		return;
	}
	
	pos = mName.rfind(AGENT_SUFFIX);
	if (pos != std::string::npos)
	{
		mName.erase(pos);
		mType = AGENT;
		return;
	}
	
	pos = mName.rfind(BY_NAME_SUFFIX);
	if (pos != std::string::npos)
	{
		mName.erase(pos);
		mType = BY_NAME;
		return;
	}
	
	llwarns << "Unable to set mute from display name " << display_name << llendl;
	return;
}

//-----------------------------------------------------------------------------
// LLMuteList()
//-----------------------------------------------------------------------------
LLMuteList::LLMuteList() :
	mIsLoaded(FALSE)
{
	LLMessageSystem* msg = gMessageSystem;

	// Register our various callbacks
	msg->setHandlerFuncFast(_PREHASH_MuteListUpdate, processMuteListUpdate);
	msg->setHandlerFuncFast(_PREHASH_UseCachedMuteList, processUseCachedMuteList);

	gGenericDispatcher.addHandler("emptymutelist", &sDispatchEmptyMuteList);
}

//-----------------------------------------------------------------------------
// ~LLMuteList()
//-----------------------------------------------------------------------------
LLMuteList::~LLMuteList()
{
}

BOOL LLMuteList::isLinden(const LLString& name) const
{
	typedef boost::tokenizer<boost::char_separator<char> > tokenizer;
	boost::char_separator<char> sep(" ");
	tokenizer tokens(name, sep);
	tokenizer::iterator token_iter = tokens.begin();
	
	if (token_iter == tokens.end()) return FALSE;
	token_iter++;
	if (token_iter == tokens.end()) return FALSE;
	
	LLString last_name = *token_iter;
	
	if (last_name == "Linden")
	{
		return TRUE;
	}
	return FALSE;
}


BOOL LLMuteList::add(const LLMute& mute)
{
	// Can't mute Lindens
	if ((mute.mType == LLMute::AGENT || mute.mType == LLMute::BY_NAME)
		&& isLinden(mute.mName))
	{
		gViewerWindow->alertXml("MuteLinden");
		return FALSE;
	}
	
	// Can't mute self.
	if (mute.mType == LLMute::AGENT
		&& mute.mID == gAgent.getID())
	{
		return FALSE;
	}
	
	if (mute.mType == LLMute::BY_NAME)
	{		
		// Can't mute empty string by name
		if (mute.mName.empty()) 
		{
			llwarns << "Trying to mute empty string by-name" << llendl;
			return FALSE;
		}

		// Null mutes must have uuid null
		if (mute.mID.notNull())
		{
			llwarns << "Trying to add by-name mute with non-null id" << llendl;
			return FALSE;
		}

		std::pair<string_set_t::iterator, bool> result = mLegacyMutes.insert(mute.mName);
		if (result.second)
		{
			llinfos << "Muting by name " << mute.mName << llendl;
			updateAdd(mute);
			notifyObservers();
			return TRUE;
		}
		else
		{
			// was duplicate
			return FALSE;
		}
	}
	else
	{
		std::pair<mute_set_t::iterator, bool> result = mMutes.insert(mute);
		if (result.second)
		{
			llinfos << "Muting " << mute.mName << " id " << mute.mID << llendl;
			updateAdd(mute);
			notifyObservers();
			//Kill all particle systems owned by muted task
			if(mute.mType == LLMute::AGENT || mute.mType == LLMute::OBJECT)
			{
				gWorldPointer->mPartSim.cleanMutedParticles(mute.mID);
			}

			return TRUE;
		}
		else
		{
			return FALSE;
		}
	}
}

void LLMuteList::updateAdd(const LLMute& mute)
{
	// Update the database
	LLMessageSystem* msg = gMessageSystem;
	msg->newMessageFast(_PREHASH_UpdateMuteListEntry);
	msg->nextBlockFast(_PREHASH_AgentData);
	msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID());
	msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID());
	msg->nextBlockFast(_PREHASH_MuteData);
	msg->addUUIDFast(_PREHASH_MuteID, mute.mID);
	msg->addStringFast(_PREHASH_MuteName, mute.mName);
	msg->addS32("MuteType", mute.mType);
	msg->addU32("MuteFlags", 0x0);	// future
	gAgent.sendReliableMessage();

	mIsLoaded = TRUE;
}


BOOL LLMuteList::remove(const LLMute& mute)
{
	BOOL found = FALSE;
	
	// First, remove from main list.
	mute_set_t::iterator it = mMutes.find(mute);
	if (it != mMutes.end())
	{
		updateRemove(*it);
		mMutes.erase(it);
		// Must be after erase.
		notifyObservers();
		found = TRUE;
	}

	// Clean up any legacy mutes
	string_set_t::iterator legacy_it = mLegacyMutes.find(mute.mName);
	if (legacy_it != mLegacyMutes.end())
	{
		// Database representation of legacy mute is UUID null.
		LLMute mute(LLUUID::null, *legacy_it, LLMute::BY_NAME);
		updateRemove(mute);
		mLegacyMutes.erase(legacy_it);
		// Must be after erase.
		notifyObservers();
		found = TRUE;
	}
	
	return found;
}


void LLMuteList::updateRemove(const LLMute& mute)
{
	LLMessageSystem* msg = gMessageSystem;
	msg->newMessageFast(_PREHASH_RemoveMuteListEntry);
	msg->nextBlockFast(_PREHASH_AgentData);
	msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID());
	msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID());
	msg->nextBlockFast(_PREHASH_MuteData);
	msg->addUUIDFast(_PREHASH_MuteID, mute.mID);
	msg->addString("MuteName", mute.mName);
	gAgent.sendReliableMessage();
}


std::vector<LLMute> LLMuteList::getMutes() const
{
	std::vector<LLMute> mutes;
	
	for (mute_set_t::const_iterator it = mMutes.begin();
		 it != mMutes.end();
		 ++it)
	{
		mutes.push_back(*it);
	}
	
	for (string_set_t::const_iterator it = mLegacyMutes.begin();
		 it != mLegacyMutes.end();
		 ++it)
	{
		LLMute legacy(LLUUID::null, *it);
		mutes.push_back(legacy);
	}
	
	std::sort(mutes.begin(), mutes.end(), compare_by_name());
	return mutes;
}

//-----------------------------------------------------------------------------
// loadFromFile()
//-----------------------------------------------------------------------------
BOOL LLMuteList::loadFromFile(const LLString& filename)
{
	FILE* fp = LLFile::fopen(filename.c_str(), "rb");
	if (!fp)
	{
		llwarns << "Couldn't open mute list " << filename << llendl;
		return FALSE;
	}

	char id_buffer[MAX_STRING];
	char name_buffer[MAX_STRING];
	char buffer[MAX_STRING];
	while (!feof(fp) 
		   && fgets(buffer, MAX_STRING, fp))
	{
		id_buffer[0] = '\0';
		name_buffer[0] = '\0';
		S32 type = 0;
		sscanf(buffer, " %d %s %[^|]", &type, id_buffer, name_buffer);
		LLUUID id = LLUUID(id_buffer);
		LLMute mute(id, name_buffer, (LLMute::EType)type);
		if (mute.mID.isNull()
			|| mute.mType == LLMute::BY_NAME)
		{
			mLegacyMutes.insert(mute.mName);
		}
		else
		{
			mMutes.insert(mute);
		}
	}
	fclose(fp);
	mIsLoaded = TRUE;
	notifyObservers();	
	return TRUE;
}

//-----------------------------------------------------------------------------
// saveToFile()
//-----------------------------------------------------------------------------
BOOL LLMuteList::saveToFile(const LLString& filename)
{
	FILE* fp = LLFile::fopen(filename.c_str(), "wb");
	if (!fp)
	{
		llwarns << "Couldn't open mute list " << filename << llendl;
		return FALSE;
	}
	// legacy mutes have null uuid
	char id_string[UUID_STR_LENGTH];
	LLUUID::null.toString(id_string);
	for (string_set_t::iterator it = mLegacyMutes.begin();
		 it != mLegacyMutes.end();
		 ++it)
	{
		fprintf(fp, "%d %s %s|\n", (S32)LLMute::BY_NAME, id_string, it->c_str());
	}
	for (mute_set_t::iterator it = mMutes.begin();
		 it != mMutes.end();
		 ++it)
	{
		it->mID.toString(id_string);
		const LLString& name = it->mName;
		fprintf(fp, "%d %s %s|\n", (S32)it->mType, id_string, name.c_str());
	}
	fclose(fp);
	return TRUE;
}


BOOL LLMuteList::isMuted(const LLUUID& id, const LLString& name) const
{
	// don't need name or type for lookup
	LLMute mute(id);
	mute_set_t::const_iterator mute_it = mMutes.find(mute);
	if (mute_it != mMutes.end()) return TRUE;

	// empty names can't be legacy-muted
	if (name.empty()) return FALSE;

	// Look in legacy pile
	string_set_t::const_iterator legacy_it = mLegacyMutes.find(name);
	return legacy_it != mLegacyMutes.end();
}

//-----------------------------------------------------------------------------
// requestFromServer()
//-----------------------------------------------------------------------------
void LLMuteList::requestFromServer(const LLUUID& agent_id)
{
	char agent_id_string[UUID_STR_LENGTH];
	char filename[LL_MAX_PATH];
	agent_id.toString(agent_id_string);
	sprintf(filename, "%s.cached_mute", gDirUtilp->getExpandedFilename(LL_PATH_CACHE,agent_id_string).c_str());
	LLCRC crc;
	crc.update(filename);

	LLMessageSystem* msg = gMessageSystem;
	msg->newMessageFast(_PREHASH_MuteListRequest);
	msg->nextBlockFast(_PREHASH_AgentData);
	msg->addUUIDFast(_PREHASH_AgentID, agent_id);
	msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID());
	msg->nextBlockFast(_PREHASH_MuteData);
	msg->addU32Fast(_PREHASH_MuteCRC, crc.getCRC());
	gAgent.sendReliableMessage();
}

//-----------------------------------------------------------------------------
// cache()
//-----------------------------------------------------------------------------

void LLMuteList::cache(const LLUUID& agent_id)
{
	// Write to disk even if empty.
	if(mIsLoaded)
	{
		char agent_id_string[UUID_STR_LENGTH];
		char filename[LL_MAX_PATH];
		agent_id.toString(agent_id_string);
		sprintf(filename, "%s.cached_mute", gDirUtilp->getExpandedFilename(LL_PATH_CACHE,agent_id_string).c_str());
		saveToFile(filename);
	}
}


//-----------------------------------------------------------------------------
// Static message handlers
//-----------------------------------------------------------------------------

void LLMuteList::processMuteListUpdate(LLMessageSystem* msg, void**)
{
	llinfos << "LLMuteList::processMuteListUpdate()" << llendl;
	LLUUID agent_id;
	msg->getUUIDFast(_PREHASH_MuteData, _PREHASH_AgentID, agent_id);
	if(agent_id != gAgent.getID())
	{
		llwarns << "Got an mute list update for the wrong agent." << llendl;
		return;
	}
	char filename[MAX_STRING];
	filename[0] = '\0';
	msg->getStringFast(_PREHASH_MuteData, _PREHASH_Filename, MAX_STRING, filename);
	
	std::string *local_filename_and_path = new std::string(gDirUtilp->getExpandedFilename( LL_PATH_CACHE, filename ));
	gXferManager->requestFile(local_filename_and_path->c_str(),
							  filename,
							  LL_PATH_CACHE,
							  msg->getSender(),
							  TRUE, // make the remote file temporary.
							  onFileMuteList,
							  (void**)local_filename_and_path,
							  LLXferManager::HIGH_PRIORITY);
}

void LLMuteList::processUseCachedMuteList(LLMessageSystem* msg, void**)
{
	llinfos << "LLMuteList::processUseCachedMuteList()" << llendl;
	if (!gMuteListp) return;

	char agent_id_string[UUID_STR_LENGTH];
	gAgent.getID().toString(agent_id_string);
	char filename[LL_MAX_PATH];
	sprintf(filename, "%s.cached_mute", gDirUtilp->getExpandedFilename(LL_PATH_CACHE,agent_id_string).c_str());
	gMuteListp->loadFromFile(filename);
}

void LLMuteList::onFileMuteList(void** user_data, S32 error_code)
{
	llinfos << "LLMuteList::processMuteListFile()" << llendl;
	if (!gMuteListp) return;

	std::string *local_filename_and_path = (std::string*)user_data;
	if(local_filename_and_path && !local_filename_and_path->empty() && (error_code == 0))
	{
		gMuteListp->loadFromFile(local_filename_and_path->c_str());
		LLFile::remove(local_filename_and_path->c_str());
	}
	delete local_filename_and_path;
}

void LLMuteList::addObserver(LLMuteListObserver* observer)
{
	mObservers.insert(observer);
}

void LLMuteList::removeObserver(LLMuteListObserver* observer)
{
	mObservers.erase(observer);
}

void LLMuteList::setLoaded()
{
	mIsLoaded = TRUE;
	notifyObservers();
}

void LLMuteList::notifyObservers()
{
	// HACK: LLFloaterMute is constructed before LLMuteList,
	// so it can't easily observe it.  Changing this requires
	// much reshuffling of the startup process. JC
	if (gFloaterMute)
	{
		gFloaterMute->refreshMuteList();
	}

	for (observer_set_t::iterator it = mObservers.begin();
		it != mObservers.end();
		)
	{
		LLMuteListObserver* observer = *it;
		observer->onChange();
		// In case onChange() deleted an entry.
		it = mObservers.upper_bound(observer);
	}
}
