/**
 * @file fsdata.cpp
 * @brief Downloadable dymatic xml data for viewer features.
 *
 * $LicenseInfo:firstyear=2013&license=viewerlgpl$
 * Phoenix Firestorm Viewer Source Code
 * Copyright (C) 2011-2013 Techwolf Lupindo
 * Portions Copyright (C)
 *   2011 Wolfspirit Magic
 *   2012 Ansariel Hiller @ Second Life
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * 
 * The Phoenix Firestorm Project, Inc., 1831 Oakwood Drive, Fairmont, Minnesota 56031-3225 USA
 * http://www.firestormviewer.org
 * $/LicenseInfo$
 */

#include "llviewerprecompiledheaders.h"

#include "fsdata.h"
#include "fscommon.h"
#include "fscorehttputil.h"
#include "fsassetblacklist.h"

/* boost: will not compile unless equivalent is undef'd, beware. */
#include "fix_macros.h"
#include <boost/filesystem.hpp>

#include "llappviewer.h"
#include "llagent.h"
#include "llagentui.h"
#include "llfloaterabout.h"
#include "llimview.h"
#include "llmutelist.h"
#include "llnotifications.h"
#include "llsdserialize.h"
#include "lltrans.h"
#include "llversioninfo.h"
#include "llviewercontrol.h"
#include "llviewermedia.h"
#include "llviewernetwork.h"
#include "llxorcipher.h"
#include "llfilesystem.h"
#include "message.h"

// [RLVa:KB]
#include "rlvactions.h"
#include "rlvhelper.h"
// [/RLVa:KB]

const std::string LEGACY_CLIENT_LIST_URL = "http://phoenixviewer.com/app/client_tags/client_list_v2.xml";
const LLUUID MAGIC_ID("3c115e51-04f4-523c-9fa6-98aff1034730");

FSData::FSData() :
	mLegacySearch(true),
	mFSDataDone(false),
	mAgentsDone(false)
{
	mHeaders.insert("User-Agent", LLViewerMedia::getInstance()->getCurrentUserAgent());
	mHeaders.insert("viewer-version", LLVersionInfo::getInstance()->getChannelAndVersionFS());
	
	mBaseURL = gSavedSettings.getBOOL("FSdataQAtest") ? "http://phoenixviewer.com/app/fsdatatest" : "http://phoenixviewer.com/app/fsdata";
	mFSDataURL = mBaseURL + "/" + "data.xml";
}

FSData::~FSData()
{
	for (avatar_name_cache_connection_map_t::iterator it = mAvatarNameCacheConnections.begin(); it != mAvatarNameCacheConnections.end(); ++it)
	{
		if (it->second.connected())
		{
			it->second.disconnect();
		}
	}
	mAvatarNameCacheConnections.clear();
}

void FSData::processResponder(const LLSD& content, const std::string& url, bool save_to_file, const LLDate& last_modified)
{
	if (url == mFSDataURL)
	{
		if (!save_to_file)
		{
			LLSD data;
			LL_DEBUGS("fsdata") << "Loading fsdata.xml from " << mFSdataFilename << LL_ENDL;
			if (loadFromFile(data, mFSdataFilename))
			{
				processData(data);
			}
			else
			{
				LL_WARNS("fsdata") << "Unable to download or load fsdata.xml" << LL_ENDL;
			}
		}
		else
		{
			processData(content);
			saveLLSD(content, mFSdataFilename, last_modified);
		}
		mFSDataDone = true;
	}
	else if (url == mAssetsURL)
	{
		if (!save_to_file)
		{
			LLSD data;
			LL_DEBUGS("fsdata") << "Loading assets.xml from  " << mAssetsFilename << LL_ENDL;
			if (loadFromFile(data, mAssetsFilename))
			{
				processAssets(data);
			}
			else
			{
				LL_WARNS("fsdata") << "Unable to download or load assets.xml" << LL_ENDL;
			}
		}
		else
		{
			processAssets(content);
			saveLLSD(content, mAssetsFilename, last_modified);
		}
	}
	else if (url == mAgentsURL)
	{
		if (!save_to_file)
		{
			LLSD data;
			LL_DEBUGS("fsdata") << "Loading agents.xml from  " << mAgentsFilename << LL_ENDL;
			if (loadFromFile(data, mAgentsFilename))
			{
				processAgents(data);
			}
			else
			{
				LL_WARNS("fsdata") << "Unable to download or load agents.xml" << LL_ENDL;
			}
		}
		else
		{
			processAgents(content);
			saveLLSD(content, mAgentsFilename, last_modified);
		}
		mAgentsDone = true;
		addAgents();
	}
	else if (url == LEGACY_CLIENT_LIST_URL)
	{
		if (!save_to_file)
		{
			updateClientTagsLocal();
		}
		else
		{
			processClientTags(content);
			saveLLSD(content, mClientTagsFilename, last_modified);
		}
	}
	else if (url == mFSdataDefaultsUrl)
	{
		if (!save_to_file)
		{
			// do nothing as this file is loaded during app startup.
		}
		else
		{
			saveLLSD(content, mFSdataDefaultsFilename, last_modified);
		}
	}
}

bool FSData::loadFromFile(LLSD& data, std::string filename)
{
	llifstream file(filename.c_str());
	if (file.is_open())
	{
		if (LLSDSerialize::fromXML(data, file) != LLSDParser::PARSE_FAILURE)
		{
			file.close();
			return true;
		}
		else
		{
			// error reading file
			file.close();
			return false;
		}
	}
	else
	{
		// file does not exist or other error
		return false;
	}
}

void downloadComplete(LLSD const &aData, std::string const &aURL, bool success)
{
	LL_DEBUGS("fsdata") << aURL << ": " << aData << " - success = " << success << LL_ENDL;
	
	LLDate lastModified;
	LLSD data;
	if (success)
	{
		LLSD header = aData[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS][LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_HEADERS];
		if (header.has("last-modified"))
		{
			lastModified.secondsSinceEpoch(FSCommon::secondsSinceEpochFromString("%a, %d %b %Y %H:%M:%S %ZP", header["last-modified"].asString()));
		}

		const LLSD::Binary& binary = aData[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_RAW].asBinary();
		std::string content;
		content.assign(binary.begin(), binary.end());
		std::istringstream raw(content);
		
		LLPointer<LLSDParser> parser = new LLSDXMLParser();
		if (parser->parse(raw, data, content.size()) == LLSDParser::PARSE_FAILURE)
		{
			LL_WARNS("fsdata") << "Error parsing data received from " << aURL << ":" << LL_NEWLINE << content << LL_ENDL;
		}

		LL_DEBUGS("fsdata") << "data: " << data << LL_ENDL;
	}

	FSData::getInstance()->processResponder(data, aURL, success, lastModified);
}

void downloadCompleteScript(LLSD const &aData, std::string const &aURL, std::string const &aFilename)
{
	LL_DEBUGS("fsdata") << aURL << ": " << aData << LL_ENDL;
	LLSD header = aData[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS][LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_HEADERS];

	LLDate lastModified;
	if (header.has("last-modified"))
	{
		lastModified.secondsSinceEpoch(FSCommon::secondsSinceEpochFromString("%a, %d %b %Y %H:%M:%S %ZP", header["last-modified"].asString()));
	}
	const LLSD::Binary &rawData = aData[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_RAW].asBinary();

	if (rawData.size() <= 0)
	{
		LL_WARNS("fsdata") << "Received zero data for " << aURL << LL_ENDL;
		return;
	}

	// basic check for valid data received
	LLXMLNodePtr xml_root;
	std::string stringData;
	stringData.assign( rawData.begin(), rawData.end() ); // LLXMLNode::parseBuffer wants a U8*, not a const U8*, so need to copy here just to be safe
	if ( (!LLXMLNode::parseBuffer( reinterpret_cast< U8*> ( &stringData[0] ), stringData.size(), xml_root, NULL)) || (xml_root.isNull()) || (!xml_root->hasName("script_library")) )
	{
		LL_WARNS("fsdata") << "Could not read the script library data from "<< aURL << LL_ENDL;
		return;
	}

	LLAPRFile outfile ;
	outfile.open(aFilename, LL_APR_WB);
	if (!outfile.getFileHandle())
	{
		LL_WARNS("fsdata") << "Unable to open file for writing: " << aFilename << LL_ENDL;
	}
	else
	{
		LL_INFOS("fsdata") << "Saving " << aFilename << LL_ENDL;
		outfile.write(  &rawData[0], rawData.size() );
		outfile.close() ;
	}
}

void downloadError(LLSD const &aData, std::string const &aURL)
{
	LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(aData);

	if (status.getType() == HTTP_NOT_MODIFIED)
	{
		LL_INFOS("fsdata") << "Didn't download " << aURL << " - no newer version available" << LL_ENDL;
	}
	else
	{
		LL_WARNS("fsdata") << "Failed to download " << aURL << ": " << aData << LL_ENDL;
	}
}

// call this just before the login screen and after the LLProxy has been setup.
void FSData::startDownload()
{
	mFSdataFilename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, "fsdata.xml");
	mFSdataDefaultsFilename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, llformat("fsdata_defaults.%s.xml", LLVersionInfo::getInstance()->getShortVersion().c_str()));
	mClientTagsFilename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, "client_list_v2.xml");

	{
		// Stat the file to see if it exists and when it was last modified.
		time_t last_modified = 0;
		llstat stat_data;
		if (!LLFile::stat(mFSdataFilename, &stat_data))
		{
			last_modified = stat_data.st_mtime;
		}
		LL_INFOS("fsdata") << "Downloading data.xml from " << mFSDataURL << " with last modified of " << last_modified << LL_ENDL;
		LLCore::HttpOptions::ptr_t httpOpts(new LLCore::HttpOptions);
		httpOpts->setWantHeaders(true);
		httpOpts->setLastModified((long)last_modified);
		FSCoreHttpUtil::callbackHttpGetRaw(mFSDataURL, boost::bind(downloadComplete, _1, mFSDataURL, true), boost::bind(downloadComplete, _1, mFSDataURL, false), LLCore::HttpHeaders::ptr_t(), httpOpts);
	}

	{
		time_t last_modified = 0;
		llstat stat_data;
		if (!LLFile::stat(mFSdataDefaultsFilename, &stat_data))
		{
			last_modified = stat_data.st_mtime;
		}
		std::string filename = llformat("defaults.%s.xml", LLVersionInfo::getInstance()->getShortVersion().c_str());
		mFSdataDefaultsUrl = mBaseURL + "/" + filename;
		LL_INFOS("fsdata") << "Downloading defaults.xml from " << mFSdataDefaultsUrl << " with last modified of " << last_modified << LL_ENDL;
		LLCore::HttpOptions::ptr_t httpOpts(new LLCore::HttpOptions);
		httpOpts->setWantHeaders(true);
		httpOpts->setLastModified((long)last_modified);
		FSCoreHttpUtil::callbackHttpGetRaw(mFSdataDefaultsUrl, boost::bind(downloadComplete, _1, mFSdataDefaultsUrl, true), boost::bind(downloadComplete, _1, mFSdataDefaultsUrl, false), LLCore::HttpHeaders::ptr_t(), httpOpts);
	}

#if OPENSIM
	std::string filenames[] = { "scriptlibrary_ossl.xml", "scriptlibrary_aa.xml" };
	for (auto const& script_name : filenames)
	{
		std::string filename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, script_name);
		time_t last_modified = 0;
		llstat stat_data;
		if (!LLFile::stat(filename, &stat_data))
		{
			last_modified = stat_data.st_mtime;
		}
		std::string url = mBaseURL + "/" + script_name;
		LL_INFOS("fsdata") << "Downloading " << script_name << " from " << url << " with last modified of " << last_modified << LL_ENDL;
		LLCore::HttpOptions::ptr_t httpOpts(new LLCore::HttpOptions);
		httpOpts->setWantHeaders(true);
		httpOpts->setLastModified((long)last_modified);
		FSCoreHttpUtil::callbackHttpGetRaw( url, boost::bind( downloadCompleteScript, _1, url, filename ), boost::bind( downloadError, _1, url ), LLCore::HttpHeaders::ptr_t(), httpOpts);
	}
#endif
}

// call this _after_ the login screen to pick up grid data.
void FSData::downloadAgents()
{
#ifdef OPENSIM
	std::string filename_prefix = LLGridManager::getInstance()->getGridId();
#else
	std::string filename_prefix = "second_life";
#endif

#ifdef OPENSIM
	if (!LLGridManager::getInstance()->isInSecondLife())
	{
		// TODO: Let the opensim devs and opensim group figure out the best way
		// to add "agents.xml" URL to the gridinfo protocol.
		//getAgentsURL();

		// there is no need for assets.xml URL for opensim grids as the grid owner can just delete
		// the bad asset itself.
	}
	else
#endif
	{
		mAgentsURL = mBaseURL + "/" + "agents.xml";
		mAssetsURL = mBaseURL + "/" + "assets.xml";
	}

	if (!mAgentsURL.empty())
	{
		mAgentsFilename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, filename_prefix + "_agents.xml");
		time_t last_modified = 0;
		llstat stat_data;
		if (!LLFile::stat(mAgentsFilename, &stat_data))
		{
			last_modified = stat_data.st_mtime;
		}
		LL_INFOS("fsdata") << "Downloading agents.xml from " << mAgentsURL << " with last modified of " << last_modified << LL_ENDL;
		LLCore::HttpOptions::ptr_t httpOpts(new LLCore::HttpOptions);
		httpOpts->setWantHeaders(true);
		httpOpts->setLastModified((long)last_modified);
		FSCoreHttpUtil::callbackHttpGetRaw(mAgentsURL, boost::bind(downloadComplete, _1, mAgentsURL, true), boost::bind(downloadComplete, _1, mAgentsURL, false), LLCore::HttpHeaders::ptr_t(), httpOpts);
	}

	if (!mAssetsURL.empty())
	{
		mAssetsFilename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, filename_prefix + "_assets.xml");
		time_t last_modified = 0;
		llstat stat_data;
		if (!LLFile::stat(mAssetsFilename, &stat_data))
		{
			last_modified = stat_data.st_mtime;
		}
		LL_INFOS("fsdata") << "Downloading assets.xml from " << mAssetsURL << " with last modified of " << last_modified << LL_ENDL;
		LLCore::HttpOptions::ptr_t httpOpts(new LLCore::HttpOptions);
		httpOpts->setWantHeaders(true);
		httpOpts->setLastModified((long)last_modified);
		FSCoreHttpUtil::callbackHttpGetRaw(mAssetsURL, boost::bind(downloadComplete, _1, mAssetsURL, true), boost::bind(downloadComplete, _1, mAssetsURL, false), LLCore::HttpHeaders::ptr_t(), httpOpts);
	}
}

void FSData::processData(const LLSD& fs_data)
{
	// Set Message Of The Day if present 
	if (fs_data.has("MOTD") && !fs_data["MOTD"].asString().empty())
	{
		mSecondLifeMOTD = fs_data["MOTD"].asString();
		gAgent.mMOTD.assign(mSecondLifeMOTD);
	}
	else if (fs_data.has("RandomMOTD") && fs_data["RandomMOTD"].isArray() && fs_data["RandomMOTD"].size() > 0) // only used if MOTD is not present or empty in the xml file.
	{
		mRandomMOTDs = fs_data["RandomMOTD"];
		LLSD::array_const_iterator iter = mRandomMOTDs.beginArray();
		gAgent.mMOTD.assign((iter + (ll_rand((S32)mRandomMOTDs.size())))->asString());
	}

	// If the event falls withen the current date, use that for MOTD instead.
	if (fs_data.has("EventsMOTD"))
	{
		const LLSD& events = fs_data["EventsMOTD"];
		for(LLSD::map_const_iterator iter = events.beginMap(); iter != events.endMap(); ++iter)
		{
			std::string name = iter->first;
			const LLSD& content = iter->second;
			LL_DEBUGS("fsdata") << "Found event MOTD: " << name << LL_ENDL;
			if (content["startDate"].asDate() < LLDate::now() && content["endDate"].asDate() > LLDate::now())
			{
				LL_INFOS("fsdata") << "Setting MOTD to " << name << LL_ENDL;
				gAgent.mMOTD.assign(content["EventMOTD"]); // note singler instead of plural above
				break; // Only use the first one found.
			}
		}
	}

	if (fs_data.has("OpensimMOTD"))
	{
		mOpenSimMOTD.assign(fs_data["OpensimMOTD"]);
	}

	if (fs_data.has("BlockedReleases"))
	{
		const LLSD& blocked = fs_data["BlockedReleases"];
		for (LLSD::map_const_iterator iter = blocked.beginMap(); iter != blocked.endMap(); ++iter)
		{
			std::string version = iter->first;
			const LLSD& content = iter->second;
			mBlockedVersions[version] = content;
			LL_DEBUGS("fsdata") << "Added " << version << " to mBlockedVersions" << LL_ENDL;
		}
	}

	processAgents(fs_data);
	processAssets(fs_data);

	// FSUseLegacyClienttags: 0=Off, 1=Local Clienttags, 2=Download Clienttags
	static LLCachedControl<U32> use_legacy_tags(gSavedSettings, "FSUseLegacyClienttags");
	if (use_legacy_tags > 1)
	{
		time_t last_modified = 0;
		llstat stat_data;
		if (!LLFile::stat(mClientTagsFilename, &stat_data))
		{
			last_modified = stat_data.st_mtime;
		}
		LL_INFOS("fsdata") << "Downloading client_list_v2.xml from " << LEGACY_CLIENT_LIST_URL << " with last modified of " << last_modified << LL_ENDL;
		LLCore::HttpOptions::ptr_t httpOpts(new LLCore::HttpOptions);
		httpOpts->setWantHeaders(true);
		httpOpts->setLastModified((long)last_modified);
		FSCoreHttpUtil::callbackHttpGetRaw(LEGACY_CLIENT_LIST_URL, boost::bind(downloadComplete, _1, LEGACY_CLIENT_LIST_URL, true), boost::bind(downloadComplete, _1, LEGACY_CLIENT_LIST_URL, false), LLCore::HttpHeaders::ptr_t(), httpOpts);
	}
	else if (use_legacy_tags)
	{
		updateClientTagsLocal();
	}

// [RLVa:KB]
	if ((RlvActions::isRlvEnabled()) && (fs_data.has("rlva_compat_list")))
	{
		RlvSettings::initCompatibilityMode(fs_data["rlva_compat_list"].asString());
	}
// [/RLVa:KB]
}

void FSData::processAssets(const LLSD& assets)
{
	if (!assets.has("assets"))
	{
		return;
	}

	const LLSD& asset = assets["assets"];
	for (LLSD::map_const_iterator itr = asset.beginMap(); itr != asset.endMap(); ++itr)
	{
		LLUUID uid = LLUUID(itr->first);
		LLXORCipher cipher(MAGIC_ID.mData, UUID_BYTES);
		cipher.decrypt(uid.mData, UUID_BYTES);
		LLSD data = itr->second;
		data["asset_permanent"] = false; // Don't save these locally!
		if (uid.isNull())
		{
			continue;
		}
		FSAssetBlacklist::instance().addNewItemToBlacklistData(uid, data, false);
		LL_DEBUGS("fsdata") << "Added " << uid << " to assets list." << LL_ENDL;
	}
}

void FSData::processAgents(const LLSD& data)
{
	if (data.has("Agents"))
	{
		const LLSD& agents = data["Agents"];
		for (LLSD::map_const_iterator iter = agents.beginMap(); iter != agents.endMap(); ++iter)
		{
			LLUUID key = LLUUID(iter->first);
			mSupportAgents[key] = iter->second.asInteger();
			LL_DEBUGS("fsdata") << "Added " << key << " with " << mSupportAgents[key] << " flag mask to mSupportAgentList" << LL_ENDL;
		}
	}
	else if (data.has("SupportAgents")) // Legacy format
	{
		const LLSD& support_agents = data["SupportAgents"];
		std::string newFormat;
		for (LLSD::map_const_iterator iter = support_agents.beginMap(); iter != support_agents.endMap(); ++iter)
		{
			LLUUID key = LLUUID(iter->first);
			mSupportAgents[key] = 0;
			const LLSD& content = iter->second;
			if(content.has("support"))
			{
				mSupportAgents[key] |= SUPPORT;
			}

			if(content.has("developer"))
			{
				mSupportAgents[key] |= DEVELOPER;
			}
			LL_DEBUGS("fsdata") << "Added Legacy " << key << " with " << mSupportAgents[key] << " flag mask to mSupportAgentList" << LL_ENDL;
			std::string text = llformat("<key>%s</key><!-- %s -->\n    <integer>%d</integer>\n", key.asString().c_str(), content["name"].asString().c_str(), mSupportAgents[key]);
			newFormat.append(text);
		}
		LL_DEBUGS("fsdata") << "New format for copy paste:\n" << newFormat << LL_ENDL;
	}

	if (data.has("SupportGroups"))
	{
		const LLSD& support_groups = data["SupportGroups"];
		for (LLSD::map_const_iterator itr = support_groups.beginMap(); itr != support_groups.endMap(); ++itr)
		{
			mSupportGroup.insert(LLUUID(itr->first));
			LL_DEBUGS("fsdata") << "Added " << itr->first << " to mSupportGroup" << LL_ENDL;
		}
	}
	
	if (data.has("TestingGroups"))
	{
		const LLSD& testing_groups = data["TestingGroups"];
		for (LLSD::map_const_iterator itr = testing_groups.beginMap(); itr != testing_groups.endMap(); ++itr)
		{
			mTestingGroup.insert(LLUUID(itr->first));
			LL_DEBUGS("fsdata") << "Added " << itr->first << " to mTestingGroup" << LL_ENDL;
		}
	}

	// The presence of just the key is enough to determine that legacy search needs to be disabled on this grid.
	if (data.has("DisableLegacySearch"))
	{
		mLegacySearch = false;
		LL_WARNS("fsdata") << "Legacy Search has been disabled." << LL_ENDL;
	}
}

void FSData::processClientTags(const LLSD& tags)
{
	if (tags.has("isComplete"))
	{
		mLegacyClientList = tags;
	}
}

// Selection rules for MOTD:
// * if main MOTD is defined, show this at login and during TPs
// * if random MOTDs are defined and main MOTD not set, show a random MOTD
//   at login and every TP
// * if event MOTD is defined, show event MOTD at login. For TPs, either
//   show main MOTD if defined, or show a random MOTD if defined
void FSData::selectNextMOTD()
{
	if (LLGridManager::instance().isInSLMain())
	{
		if (!mSecondLifeMOTD.empty())
		{
			gAgent.mMOTD.assign(mSecondLifeMOTD);
		}
		else if (mRandomMOTDs.isArray() && mRandomMOTDs.size() > 0)
		{
			LLSD::array_const_iterator iter = mRandomMOTDs.beginArray();
			gAgent.mMOTD.assign((iter + (ll_rand((S32)mRandomMOTDs.size())))->asString());
		}
	}
}

//WS: Create a new LLSD based on the data from the mLegacyClientList if
LLSD FSData::resolveClientTag(const LLUUID& id, bool new_system, const LLColor4& color)
{
	LLSD curtag;
	curtag["uuid"] = id.asString();
	curtag["id_based"] = new_system;
	curtag["tex_color"] = color.getValue();

	static const LLUUID id_singularity("f25263b7-6167-4f34-a4ef-af65213b2e39");
	static const LLUUID id_kokua("4b6f6b75-bf77-d1ff-0000-000000000000");
	static const LLUUID id_radegast("b748af88-58e2-995b-cf26-9486dea8e830");
	static const LLUUID id_imprudence("cc7a030f-282f-c165-44d2-b5ee572e72bf");
	static const LLUUID id_teapot("7eab0700-f000-0000-0000-546561706f7");
	static const LLUUID id_lindenlab("c228d1cf-4b5d-4ba8-84f4-899a0796aa97");
	
	static LLCachedControl<U32> client_tag_visibility(gSavedSettings, "FSClientTagsVisibility");
	static LLCachedControl<U32> use_legacy_client_tags(gSavedSettings, "FSUseLegacyClienttags");
	static LLCachedControl<U32> color_client_tags(gSavedSettings, "FSColorClienttags");
	// If we don't want to display anything...return
	if (!client_tag_visibility)
	{
		return curtag;
	}

	//WS: Do we want to use Legacy Clienttags?
	if (use_legacy_client_tags)
	{
		if (mLegacyClientList.has(id.asString()))
		{
			curtag = mLegacyClientList[id.asString()];
		}
		else
		{
			if (id == id_singularity)
			{
				curtag["name"] = "Singularity";
			}
			else if (id == id_kokua)
			{
				curtag["name"] = "Kokua";
			}
			else if (id == id_radegast)
			{
				curtag["name"] = "Radegast";
			}
			else if (id == id_imprudence)
			{
				curtag["name"] = "Imprudence";
			}
			else if (id == id_teapot)
			{
				curtag["name"] = "Teapot";
			}
			/// [FS:CR] Since SL Viewer can't connect to Opensim, and client tags only work on OpenSim
			/// it doesn't make much sense to tag V3-based viewers as SL Viewer.
			/*if (id == id_lindenlab)
			{
				curtag["name"] = "SL Viewer";
			}*/
			if (curtag.has("name")) curtag["tpvd"] = true;
		}
	}

	// Filtering starts here:
	//WS: If we have a tag using the new system, check if we want to display it's name and/or color
	if (new_system)
	{
		if (client_tag_visibility >= 3)
		{
			U32 tag_len = strnlen((const char*)&id.mData[0], UUID_BYTES);
			std::string clienttagname = std::string((const char*)&id.mData[0], tag_len);
			LLStringFn::replace_ascii_controlchars(clienttagname, LL_UNKNOWN_CHAR);
			curtag["name"] = clienttagname;
		}

		if (color_client_tags >= 3 || curtag["tpvd"].asBoolean())
		{
			if (curtag["tpvd"].asBoolean() && color_client_tags < 3)
			{
				if (color == LLColor4::blue ||
					color == LLColor4::yellow ||
					color == LLColor4::purple ||
					color == LLColor4(0.99f, 0.39f, 0.12f, 1.f) ||
					color == LLColor4::red ||
					color == LLColor4(0.99f, 0.56f, 0.65f, 1.f) ||
					color == LLColor4::white ||
					color == LLColor4::green)
				{
					curtag["color"] = color.getValue();
				}
			}
			else 
			{
				curtag["color"] = color.getValue();
			}
		}
	}

	//If we only want to display tpvd viewer. And "tpvd" is not available or false, then
	// clear the data, but keep the basedata (like uuid, id_based and tex_color) for (maybe) later displaying.
	if (client_tag_visibility <= 1 && (!curtag.has("tpvd") || !curtag["tpvd"].asBoolean()))
	{
		curtag.clear();
	}

	curtag["uuid"] = id.asString();
	curtag["id_based"] = new_system;
	curtag["tex_color"] = color.getValue();

	return curtag;
}

void FSData::updateClientTagsLocal()
{
	LLSD data;
	LL_DEBUGS("fsdata") << "Loading client_list_v2.xml from  " << mClientTagsFilename << LL_ENDL;
	if (loadFromFile(data, mClientTagsFilename))
	{
		processClientTags(data);
	}
	else
	{
		LL_WARNS("fsdata") << "Unable to download or load client_list_v2.xml" << LL_ENDL;
	}
}

void FSData::saveLLSD(const LLSD& data, const std::string& filename, const LLDate& last_modified)
{
	LL_INFOS("fsdata") << "Saving " << filename << LL_ENDL;
	llofstream file;
	file.open(filename.c_str());
	if (!file.is_open())
	{
		LL_WARNS("fsdata") << "Unable to open " << filename << LL_ENDL;
		return;
	}
	if (!LLSDSerialize::toPrettyXML(data, file))
	{
		LL_WARNS("fsdata") << "Failed to save LLSD for " << filename << LL_ENDL;
	}
	file.close();

	const std::time_t new_time = last_modified.secondsSinceEpoch();

#ifdef LL_WINDOWS
	boost::filesystem::last_write_time(boost::filesystem::path(utf8str_to_utf16str(filename)), new_time);
#else
	boost::filesystem::last_write_time(boost::filesystem::path(filename), new_time);
#endif
}

S32 FSData::getAgentFlags(const LLUUID& avatar_id)
{
	std::map<LLUUID, S32>::iterator iter = mSupportAgents.find(avatar_id);
	if (iter == mSupportAgents.end())
	{
		return -1;
	}
	return iter->second;
}

bool FSData::isSupport(const LLUUID& avatar_id)
{
	S32 flags = getAgentFlags(avatar_id);
	return (flags != -1 && (flags & SUPPORT));
}

bool FSData::isDeveloper(const LLUUID& avatar_id)
{
	S32 flags = getAgentFlags(avatar_id);
	return (flags != -1 && (flags & DEVELOPER));
}

bool FSData::isQA(const LLUUID& avatar_id)
{
	S32 flags = getAgentFlags(avatar_id);
	return (flags != -1 && (flags & QA));
}

LLSD FSData::allowedLogin()
{
	std::map<std::string, LLSD>::iterator iter = mBlockedVersions.find(LLVersionInfo::getInstance()->getChannelAndVersionFS());
	if (iter == mBlockedVersions.end())
	{
		return LLSD();
	}
	else
	{
		LLSD block = iter->second;
		bool blocked = true; // default is to block all unless there is a gridtype or grids present.
		if (block.has("gridtype"))
		{
			blocked = false;
#ifdef OPENSIM
			if ((block["gridtype"].asString() == "opensim") && LLGridManager::getInstance()->isInOpenSim())
			{
				return block;
			}
#endif
			if ((block["gridtype"].asString() == "secondlife") && LLGridManager::getInstance()->isInSecondLife())
			{
				return block;
			}
		}
		if (block.has("grids"))
		{
			blocked = false;
			LLSD grids = block["grids"];
			for (LLSD::array_iterator grid_iter = grids.beginArray(); grid_iter != grids.endArray(); ++grid_iter)
			{
				if ((*grid_iter).asString() == LLGridManager::getInstance()->getGrid())
				{
					return block;
				}
			}
		}
		return blocked ? block : LLSD();
	}
}

bool FSData::isFirestormGroup(const LLUUID& id)
{
	return isSupportGroup(id) || isTestingGroup(id);
}

bool FSData::isSupportGroup(const LLUUID& id)
{
	return mSupportGroup.count(id);
}

bool FSData::isTestingGroup(const LLUUID& id)
{
	return mTestingGroup.count(id);
}

bool FSData::isAgentFlag(const LLUUID& agent_id, flags_t flag)
{
	std::map<LLUUID, S32>::iterator iter = mSupportAgents.find(agent_id);
	if (iter == mSupportAgents.end())
	{
		return false;
	}
	return (iter->second & flag);
}

void FSData::onNameCache(const LLUUID& av_id, const LLAvatarName& av_name)
{
	avatar_name_cache_connection_map_t::iterator it = mAvatarNameCacheConnections.find(av_id);
	if (it != mAvatarNameCacheConnections.end())
	{
		if (it->second.connected())
		{
			it->second.disconnect();
		}
		mAvatarNameCacheConnections.erase(it);
	}
	LLMute mute(av_id, av_name.getUserName(), LLMute::EXTERNAL);
	LLMuteList::getInstance()->add(mute);
}

// this is called in two different places due to can recieved .xml before gCacheName is created and vice versa.
void FSData::addAgents()
{
	if (!gCacheName)
	{
		return;
	}

	for (std::map<LLUUID, S32>::iterator iter = mSupportAgents.begin(); iter != mSupportAgents.end(); ++iter)
	{
		if (iter->second & NO_SPAM)
		{
			LLUUID id = iter->first;
			avatar_name_cache_connection_map_t::iterator it = mAvatarNameCacheConnections.find(id);
			if (it != mAvatarNameCacheConnections.end())
			{
				if (it->second.connected())
				{
					it->second.disconnect();
				}
				mAvatarNameCacheConnections.erase(it);
			}

			LLAvatarNameCache::callback_connection_t cb = LLAvatarNameCache::get(id, boost::bind(&FSData::onNameCache, this, _1, _2));
			mAvatarNameCacheConnections.insert(std::make_pair(id, cb));
		}
	}
}

std::string FSData::processRequestForInfo(const LLUUID& requester, const std::string& message, const std::string& name, const LLUUID& sessionid)
{
	const std::string detectstring = "/reqsysinfo";
	if (message.find(detectstring) != 0)
	{
		return message;
	}

	if (!isSupport(requester) && !isDeveloper(requester) && !isQA(requester))
	{
		return message;
	}

	std::string outmessage = LLTrans::getString("Reqsysinfo_Chat_NoReason");
	std::string reason("");
	if (message.length() > detectstring.length())
	{
		//there is more to it!
		reason = message.substr(detectstring.length());
		LLStringUtil::format_map_t reason_args;
		reason_args["REASON"] = reason;
		outmessage = LLTrans::getString("Reqsysinfo_Chat_Reason", reason_args);
		reason = LLTrans::getString("Reqsysinfo_Reason", reason_args);
	}
	
	LLSD args;
	args["REASON"] = reason;
	args["NAME"] = name;
	LLNotifications::instance().add("FireStormReqInfo", args, LLSD().with("from_id", requester).with("session_id", sessionid), callbackReqInfo);

	return outmessage;
}

//static
void FSData::sendInfo(const LLUUID& destination, const LLUUID& sessionid, const std::string& my_name, EInstantMessage dialog)
{
	const LLSD system_info = getSystemInfo();
	const std::string part1 = system_info["Part1"].asString();
	const std::string part2 = system_info["Part2"].asString();

	pack_instant_message(
		gMessageSystem,
		gAgentID,
		FALSE,
		gAgentSessionID,
		destination,
		my_name,
		part1,
		IM_ONLINE,
		dialog,
		sessionid
		);
	gAgent.sendReliableMessage();
	pack_instant_message(
		gMessageSystem,
		gAgentID,
		FALSE,
		gAgentSessionID,
		destination,
		my_name,
		part2,
		IM_ONLINE,
		dialog,
		sessionid
		);
	gAgent.sendReliableMessage();

	LLStringUtil::format_map_t args;
	args["DATA"] = part1 + "\n" + part2;
	gIMMgr->addMessage(gIMMgr->computeSessionID(dialog, destination), destination, my_name,
						LLTrans::getString("Reqsysinfo_Chat_Information_sent", args));
}

//static
void FSData::callbackReqInfo(const LLSD &notification, const LLSD &response)
{
	S32 option = LLNotification::getSelectedOption(notification, response);
	std::string my_name;
	LLUUID from_id = notification["payload"]["from_id"].asUUID();
	LLUUID session_id = notification["payload"]["session_id"].asUUID();

	LLAgentUI::buildFullname(my_name);

	if (option == 0) //yes
	{
		sendInfo(from_id, session_id, my_name, IM_NOTHING_SPECIAL);
	}
	else
	{
		pack_instant_message(
			gMessageSystem,
			gAgentID,
			FALSE,
			gAgentSessionID,
			from_id,
			my_name,
			"Request Denied.", // Left English intentionally as it gets sent to the support staff
			IM_ONLINE,
			IM_NOTHING_SPECIAL,
			session_id
			);
		gAgent.sendReliableMessage();
		gIMMgr->addMessage(session_id, from_id, my_name, LLTrans::getString("Reqsysinfo_Chat_Request_Denied"));
	}
}

//static
LLSD FSData::getSystemInfo()
{
	LLSD info = LLAppViewer::instance()->getViewerInfo();

	std::string sysinfo1("\n");
	sysinfo1 += llformat("%s %s (%d) %s %s (%s %dbit / %s) %s\n\n",	LLAppViewer::instance()->getSecondLifeTitle().c_str(),
																	LLVersionInfo::getInstance()->getShortVersion().c_str(),
																	LLVersionInfo::getInstance()->getBuild(),
																	info["BUILD_DATE"].asString().c_str(), info["BUILD_TIME"].asString().c_str(),
																	LLVersionInfo::getInstance()->getChannel().c_str(),
																	info["ADDRESS_SIZE"].asInteger(),
																	info["SIMD"].asString().c_str(),
																	info["BUILD_TYPE"].asString().c_str());
	sysinfo1 += llformat("Build with %s version %s\n\n", info["COMPILER"].asString().c_str(), info["COMPILER_VERSION"].asString().c_str());
	sysinfo1 += llformat("I am in %s located at %s (%s)\n", info["REGION"].asString().c_str(), info["HOSTNAME"].asString().c_str(), info["HOSTIP"].asString().c_str());
	sysinfo1 += llformat("%s\n\n", info["SERVER_VERSION"].asString().c_str());

	sysinfo1 += llformat("CPU: %s\n", info["CPU"].asString().c_str());
	sysinfo1 += llformat("Memory: %d MB\n", info["MEMORY_MB"].asInteger());
	sysinfo1 += llformat("OS: %s\n", info["OS_VERSION"].asString().c_str());
	sysinfo1 += llformat("Graphics Card Vendor: %s\n", info["GRAPHICS_CARD_VENDOR"].asString().c_str());
	sysinfo1 += llformat("Graphics Card: %s\n", info["GRAPHICS_CARD"].asString().c_str());
	sysinfo1 += llformat("Graphics Card Memory: %d MB\n", info["GRAPHICS_CARD_MEMORY"].asInteger());

	if (info.has("GRAPHICS_DRIVER_VERSION"))
	{
		sysinfo1 += llformat("Graphics Card Driver Version: %s\n", info["GRAPHICS_DRIVER_VERSION"].asString().c_str());
	}

	std::string sysinfo2("\n");
	sysinfo2 += llformat("OpenGL Version: %s\n\n", info["OPENGL_VERSION"].asString().c_str());
	sysinfo2 += llformat("libcurl Version: %s\n", info["LIBCURL_VERSION"].asString().c_str());
	sysinfo2 += llformat("J2C Decoder Version: %s\n", info["J2C_VERSION"].asString().c_str());
	sysinfo2 += llformat("Audio Driver Version: %s\n", info["AUDIO_DRIVER_VERSION"].asString().c_str());
	sysinfo2 += llformat("%s\n", info["LIBCEF_VERSION"].asString().c_str());
	sysinfo2 += llformat("LibVLC Version: %s\n", info["LIBVLC_VERSION"].asString().c_str());

	sysinfo2 += llformat("Vivox Version: %s\n", info["VOICE_VERSION"].asString().c_str());
	sysinfo2 += llformat("Packets Lost: %.0f/%.0f (%.1f%%)\n\n", info["PACKETS_LOST"].asReal(), info["PACKETS_IN"].asReal(), info["PACKETS_PCT"].asReal());

	sysinfo2 += llformat("RLVa: %s\n", info["RLV_VERSION"].asString().c_str());
	sysinfo2 += llformat("Mode: %s\n", info["MODE"].asString().c_str());
	sysinfo2 += llformat("Skin: %s (%s)\n", info["SKIN"].asString().c_str(), info["THEME"].asString().c_str());
	sysinfo2 += llformat("Window Size: %sx%s px\n", info["WINDOW_WIDTH"].asString().c_str(), info["WINDOW_HEIGHT"].asString().c_str());
#if LL_DARWIN
	sysinfo2 += llformat("HiDPI: %s\n", info["HIDPI"].asBoolean() ? "Enabled" : "Disabled");
#endif
	sysinfo2 += llformat("Font: %s\n", info["FONT"].asString().c_str());
	sysinfo2 += llformat("Font Size Adjustment: %d pt\n", info["FONT_SIZE"].asInteger());
	sysinfo2 += llformat("Font Screen DPI: %d\n", info["FONT_SCREEN_DPI"].asInteger());
	sysinfo2 += llformat("UI Scaling: %.3f\n", info["UI_SCALE_FACTOR"].asReal());
	sysinfo2 += llformat("Draw Distance: %d m\n", info["DRAW_DISTANCE"].asInteger());
	sysinfo2 += llformat("Bandwidth: %d kbit/s\n", info["BANDWIDTH"].asInteger());
	sysinfo2 += llformat("LOD Factor: %.3f\n", info["LOD"].asReal());
	sysinfo2 += llformat("Render quality: %s\n", info["RENDERQUALITY_FSDATA_ENGLISH"].asString().c_str());
	sysinfo2 += llformat("ALM enabled: %s\n", info["ALMSTATUS_FSDATA_ENGLISH"].asString().c_str());
	if (info["TEXTUREMEMORYDYNAMIC"].asBoolean())
	{
		sysinfo2 += llformat("Texture memory: Dynamic (%d MB min / %d%% Cache / %d%% VRAM)\n", info["TEXTUREMEMORYMIN"].asInteger(), info["TEXTUREMEMORYCACHERESERVE"].asInteger(), info["TEXTUREMEMORYGPURESERVE"].asInteger());
	}
	else
	{
		sysinfo2 += llformat("Texture memory: %d MB (%.2f)\n", info["TEXTUREMEMORY"].asInteger(), info["TEXTUREMEMORYMULTIPLIER"].asReal());
	}
	sysinfo2 += "Disk cache: " + info["DISK_CACHE_INFO"].asString();

	LLSD sysinfos;
	sysinfos["Part1"] = sysinfo1;
	sysinfos["Part2"] = sysinfo2;

	return sysinfos;
}
