#include "precompiled.h"

#include "CLocale.h"
#include "TSComponent.h"
#include "ps/StringConvert.h"


#include <algorithm>

#include "ps/CLogger.h"
#define LOG_CATEGORY "i18n"

using namespace I18n;

// Vaguely useful utility function for deleting stuff
template<typename T> void delete_fn(T* v) { delete v; }

// These could be optimised for little-endian sizeof(wchar_t)==2 systems:
static inline void ReadWString8_(const char*& data, Str& str)
{
	u8 length = *(u8*)data;
	data += 1;
	StringConvert::ucs2le_to_wstring(data, data+length, str);
	data += length;
}
static inline void ReadWString16_(const char*& data, Str& str)
{
	u16 length = *(u16*)data;
	data += 2;
	StringConvert::ucs2le_to_wstring(data, data+length, str);
	data += length;
}

#define ReadWString8(s) Str s; ReadWString8_(data, s);
#define ReadWString16(s) Str s; ReadWString16_(data, s);


bool CLocale::LoadStrings(const char* data)
{
	// TODO: More robust file format (so errors can be detected in a
	// nicer way than watching for access violations)

	u16 PhraseCount = *(u16*)data;
	data += 2;

	for (int i = 0; i < PhraseCount; ++i)
	{
		ReadWString16(Key);

		u8 VarCount = *(u8*)data;
		data += 1;

		// Get the relevant entry in the string hash, creating it if it doesn't exist
		TranslatedString* String = Strings[Key];
		if (! String)
			String = Strings[Key] = new TranslatedString;
		// If this is a redefined string, make sure it's empty
		String->Parts.clear();
		// Store the number of variables, so translate(x)<<y<<z can check for validity
		String->VarCount = VarCount;

		u8 SectionCount = *(u8*)data;
		data += 1;

		for (int j = 0; j < SectionCount; ++j)
		{
			u8 SectionType = *(u8*)data;
			data += 1;

			switch (SectionType)
			{
			case 0: // Constant string
				{
					ReadWString16(StringText);

					String->Parts.push_back(new TSComponentString(StringText.c_str()));
					break;
				}

			case 1: // Variable
				{
					u8 VarID = *(u8*)data;
					data += 1;

					String->Parts.push_back(new TSComponentVariable(VarID));

					break;
				}

			case 2: // Function
				{
					u8 NameLength = *(u8*)data;
					data += 1;

					std::string NameText ((char*)data, (char*)(data + NameLength));
					data += NameLength;

					u8 ParamCount = *(u8*)data;
					data += 1;

					TSComponentFunction* Func = new TSComponentFunction(NameText.c_str());

					for (int k = 0; k < ParamCount; ++k)
					{
						u8 ParamType = *(u8*)data;
						data += 1;

						switch (ParamType)
						{
						case 0: // String
							{
								ReadWString8(StrText);

								Func->AddParam(new ScriptValueString(Script, StrText.c_str()));

								break;
							}

						case 1: // Variable
							{
								u8 ID = *(u8*)data;
								data += 1;

								Func->AddParam(new ScriptValueVariable(Script, ID));

								break;
							}

						case 2: // Integer
							{
								u32 Num = *(u32*)data;
								data += 4;

								Func->AddParam(new ScriptValueInteger(Script, Num));

								break;
							}

						default: // Argh!
							debug_warn("Invalid function parameter type");
						}
					}

					String->Parts.push_back(Func);

					break;
				}

			default: // Argh!
				debug_warn("Invalid translation string section type");
			}
		}
	}

	return true;
}


bool CLocale::LoadFunctions(const char* data, size_t len, const char* filename)
{
	// Insist on little-endian UTF16 files containing a BOM (e.g. as generated
	// by Notepad when saving in Unicode format)

	// TODO: Support more Unicode file formats

	if (len < 2)
	{
		LOG(ERROR, LOG_CATEGORY, "I18n: Functions file '%s' is too short", filename);
		return false;
	}

	if (*(jschar*)data != 0xFEFF)
	{
		LOG(ERROR, LOG_CATEGORY, "I18n: Functions file '%s' has invalid Unicode format (lacking little-endian BOM)", filename);
		return false;
	}

	if (! Script.ExecuteCode((jschar*)(data+2), len/2, filename))
	{
		LOG(ERROR, LOG_CATEGORY, "I18n: JS errors in functions file '%s'", filename);
		return false;
	}

	return true;
}


bool CLocale::LoadDictionary(const char* data)
{
	ReadWString8(DictName);

	u8 PropertyCount = *(u8*)data;
	data += 1;

	DictData& dict = Dictionaries[DictName];

	if (dict.DictProperties.size() && PropertyCount != dict.DictProperties.size())
	{
		LOG(ERROR, LOG_CATEGORY, "I18n: Multiple dictionary files loaded with the name ('%ls') and different properties", DictName.c_str());
		return false;
		// TODO: Check headings to make sure they're identical (or handle them more cleverly)
	}

	// Read the names of the properties

	int i;

	for (i = 0; i < PropertyCount; ++i)
	{
		ReadWString8(Property);
		dict.DictProperties[Property] = i;
	}

	u16 ValueCount = *(u16*)data;
	data += 2;

	// Read each 'value' (word + properties)

	for (i = 0; i < ValueCount; ++i)
	{
		ReadWString8(Word);

		std::vector<std::wstring>& props = dict.DictWords[Word];

		for (int j = 0; j < PropertyCount; ++j)
		{
			ReadWString8(Value);
			props.push_back(Value);
		}
	}

	return true;
}

void CLocale::UnloadDictionaries()
{
	Dictionaries.clear();
}


const CLocale::LookupType* CLocale::LookupWord(const Str& dictname, const Str& word)
{
	std::map<Str, DictData>::const_iterator dictit = Dictionaries.find(dictname);
	if (dictit == Dictionaries.end())
	{
		LOG(WARNING, LOG_CATEGORY, "I18n: Non-loaded dictionary '%ls' accessed", dictname.c_str());
		return NULL;
	}
	std::map<Str, std::vector<Str> >::const_iterator wordit = dictit->second.DictWords.find(word);
	if (wordit == dictit->second.DictWords.end())
	{
		// Word not found. Respond quietly, so JS code can handle missing
		// words in a more appropriate way.
		return NULL;
	}

	// Return some data that can later be passed to LookupProperty
	return new LookupType(&dictit->second, &wordit->second);
}

bool CLocale::LookupProperty(const LookupType* data, const Str& property, Str& result)
{
	std::map<Str, int>::const_iterator propit = data->first->DictProperties.find(property);
	if (propit == data->first->DictProperties.end())
		return false;

	// Return the appropriate string
	result = (*data->second)[propit->second];
	return true;
}




const StrImW CLocale::CallFunction(const char* name, const std::vector<BufferVariable*>& vars, const std::vector<ScriptValue*>& params)
{
	return Script.CallFunction(name, vars, params);
}



StringBuffer CLocale::Translate(const wchar_t* id)
{
	if (++CacheAge > CacheAgeLimit)
	{
		CacheAge = 0;
		ClearCache();
	}

	StringsType::iterator TransStr = Strings.find(Str(id));
	if (TransStr == Strings.end())
	{
		LOG(WARNING, LOG_CATEGORY, "I18n: No translation found for string '%ls'", id);

		// Just use the ID string directly, and remember it for the future
		return StringBuffer(&AddDefaultString(id), this);
	}

	return StringBuffer((*TransStr).second, this);
}


void CLocale::AddToCache(StringBuffer* sb, Str& str)
{
	CacheData& d = TranslationCache[sb->String];

	// Clean up any earlier cache entry
	std::for_each(d.vars.begin(), d.vars.end(), delete_fn<BufferVariable>);

	// Set the data for the new cache entry
	d.hash = sb->Hash();
	d.vars = sb->Variables;
	d.output = str;
}

bool CLocale::ReadCached(StringBuffer* sb, Str& str)
{
	// Look for a string with the right key in the cache
	std::map<TranslatedString*, CacheData>::iterator it = 
		TranslationCache.find(sb->String);

	// See if it actually exists
	if (it == TranslationCache.end())
		return false;

	// Check quickly whether the hashes match
	if (sb->Hash() != (*it).second.hash)
		return false;

	// Check every variable to see whether they're identical
	debug_assert(sb->Variables.size() == (*it).second.vars.size()); // this should always be true
	size_t count = sb->Variables.size();
	for (size_t i = 0; i < count; ++i)
		if (! ( *sb->Variables[i] == *(*it).second.vars[i] ) )
			return false;

	str = (*it).second.output;
	return true;
}

void CLocale::ClearCache()
{
	// Deallocate cached data
	for (std::map<TranslatedString*, CacheData>::iterator it = TranslationCache.begin(); it != TranslationCache.end(); ++it)
		std::for_each((*it).second.vars.begin(), (*it).second.vars.end(), delete_fn<BufferVariable>);

	TranslationCache.clear();
}


bool is_valid_variable_char(wchar_t c)
{
	// c =~ /[a-zA-Z0-9_]/
	// (Hurrah for internationalisation.)
	return
		(c >= 'a' && c <= 'z')
	||	(c >= 'A' && c <= 'Z')
	||	(c >= '0' && c <= '9')
	||	(c == '_');
}

TranslatedString& CLocale::AddDefaultString(const wchar_t* id)
{
	// Parse a string involving $variables and $$ (=$)

	enum ParseState {
		st_default,
		st_afterdollar,
		st_variable
	};
	ParseState state = st_default;

	TranslatedString* str = new TranslatedString;
	str->VarCount = 0;

	std::wstring tempstr;

	for (const wchar_t* ch = id; *ch != '\0'; ++ch)
	{
		switch (state)
		{
		case st_default:
			if (*ch == '$')
			{
				state = st_afterdollar;
			}
			else
			{
				tempstr += *ch;
			}

			break;
		case st_afterdollar:
			if (*ch == '$')
			{
				tempstr += '$';
				state = st_default;
			}
			else
			{
				// Start of a variable name.
				// Push the old string onto the component stack
				if (tempstr.length())
				{
					str->Parts.push_back(new TSComponentString(tempstr.c_str()));
					tempstr.clear();
				}

				// Set the ID (starting at 0) and increment the count
				str->Parts.push_back(new TSComponentVariable(str->VarCount++));
				
				state = st_variable;
			}
			break;
		case st_variable:
			if (*ch == '$')
			{
				state = st_afterdollar;
			}
			else if (! is_valid_variable_char(*ch))
			{
				state = st_default;
				tempstr = *ch;
			}
			// We don't care about the actual name of the variable, so just ignore it.

			break;
		}
	}
	// Make sure the last string is added to the parts list
	if (tempstr.length())
		str->Parts.push_back(new TSComponentString(tempstr.c_str()));

	Strings[id] = str;

	return *str;
}


CLocale::~CLocale()
{
	// Clean up the list of strings
	for (StringsType::iterator it = Strings.begin(); it != Strings.end(); ++it)
		delete (*it).second;

	ClearCache();
}
