/*
===========================================================================

Doom 3 BFG Edition GPL Source Code
Copyright (C) 1993-2012 id Software LLC, a ZeniMax Media company.

This file is part of the Doom 3 BFG Edition GPL Source Code ("Doom 3 BFG Edition Source Code").

Doom 3 BFG Edition Source Code 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, either version 3 of the License, or
(at your option) any later version.

Doom 3 BFG Edition Source Code 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 Doom 3 BFG Edition Source Code.  If not, see <http://www.gnu.org/licenses/>.

In addition, the Doom 3 BFG Edition Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 BFG Edition Source Code.  If not, please request a copy in writing from id Software at the address below.

If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.

===========================================================================
*/
#pragma hdrstop
#include "precompiled.h"

// This is the default language dict that the entire system uses, but you can instantiate your own idLangDict classes to manipulate a language dictionary in a tool
idLangDict	idLocalization::languageDict;

idCVar lang_maskLocalizedStrings( "lang_maskLocalizedStrings", "0", CVAR_BOOL, "Masks all localized strings to help debugging.  When set will replace strings with an equal length of W's and ending in an X.  Note: The masking occurs at string table load time." );

/*
========================
idLocalization::ClearDictionary
========================
*/
void idLocalization::ClearDictionary()
{
	languageDict.Clear();
}

/*
========================
idLocalization::LoadDictionary
========================
*/
bool idLocalization::LoadDictionary( const byte* data, int dataLen, const char* fileName )
{
	return languageDict.Load( data, dataLen, fileName );
}

/*
========================
idLocalization::GetString
========================
*/
const char* idLocalization::GetString( const char* inString )
{
	return languageDict.GetString( inString );
}

/*
========================
idLocalization::FindString
========================
*/
const char* idLocalization::FindString( const char* inString )
{
	return languageDict.FindString( inString );
}

/*
========================
idLocalization::VerifyUTF8
========================
*/
utf8Encoding_t idLocalization::VerifyUTF8( const uint8* buffer, const int bufferLen, const char* name )
{
	utf8Encoding_t encoding;
	idStr::IsValidUTF8( buffer, bufferLen, encoding );
	if( encoding == UTF8_INVALID )
	{
		idLib::FatalError( "Language file %s is not valid UTF-8 or plain ASCII.", name );
	}
	else if( encoding == UTF8_INVALID_BOM )
	{
		idLib::FatalError( "Language file %s is marked as UTF-8 but has invalid encoding.", name );
	}
	else if( encoding == UTF8_ENCODED_NO_BOM )
	{
		idLib::FatalError( "Language file %s has no byte order marker. Fix this or roll back to a version that has the marker.", name );
	}
	else if( encoding != UTF8_ENCODED_BOM && encoding != UTF8_PURE_ASCII )
	{
		idLib::FatalError( "Language file %s has unknown utf8Encoding_t.", name );
	}
	return encoding;
}

// string entries can refer to other string entries,
// recursing up to this many times before we decided someone did something stupid
const char* idLangDict::KEY_PREFIX = "#str_";	// all keys should be prefixed with this for redirection to work
const int idLangDict::KEY_PREFIX_LEN = idStr::Length( KEY_PREFIX );

/*
========================
idLangDict::idLangDict
========================
*/
idLangDict::idLangDict() : keyIndex( 4096, 4096 )
{
}

/*
========================
idLangDict::~idLangDict
========================
*/
idLangDict::~idLangDict()
{
	Clear();
}

/*
========================
idLangDict::Clear
========================
*/
void idLangDict::Clear()
{
	//mem.PushHeap();
	for( int i = 0; i < keyVals.Num(); i++ )
	{
		if( keyVals[i].value == NULL )
		{
			continue;
		}
		blockAlloc.Free( keyVals[i].value );
		keyVals[i].value = NULL;
	}
	//mem.PopHeap();
}

/*
========================
idLangDict::Load
========================
*/
bool idLangDict::Load( const byte* buffer, const int bufferLen, const char* name )
{

	if( buffer == NULL || bufferLen <= 0 )
	{
		// let whoever called us deal with the failure (so sys_lang can be reset)
		return false;
	}
	
	idLib::Printf( "Reading %s", name );
	
	bool utf8 = false;
	
	// in all but retail builds, ensure that the byte-order mark is NOT MISSING so that
	// we can avoid debugging UTF-8 code
#ifndef ID_RETAIL
	utf8Encoding_t encoding = idLocalization::VerifyUTF8( buffer, bufferLen, name );
	if( encoding == UTF8_ENCODED_BOM )
	{
		utf8 = true;
	}
	else if( encoding == UTF8_PURE_ASCII )
	{
		utf8 = false;
	}
	else
	{
		assert( false );	// this should have been handled in VerifyUTF8 with a FatalError
		return false;
	}
#else
	// in release we just check the BOM so we're not scanning the lang file twice on startup
	if( bufferLen > 3 && buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0xBF )
	{
		utf8 = true;
	}
#endif
	
	if( utf8 )
	{
		idLib::Printf( " as UTF-8\n" );
	}
	else
	{
		idLib::Printf( " as ASCII\n" );
	}
	
	idStr tempKey;
	idStr tempVal;
	
	int line = 0;
	int numStrings = 0;
	
	int i = 0;
	while( i < bufferLen )
	{
		uint32 c = buffer[i++];
		if( c == '/' )    // comment, read until new line
		{
			while( i < bufferLen )
			{
				c = buffer[i++];
				if( c == '\n' )
				{
					line++;
					break;
				}
			}
		}
		else if( c == '}' )
		{
			break;
		}
		else if( c == '\n' )
		{
			line++;
		}
		else if( c == '\"' )
		{
			int keyStart = i;
			int keyEnd = -1;
			while( i < bufferLen )
			{
				c = buffer[i++];
				if( c == '\"' )
				{
					keyEnd = i - 1;
					break;
				}
			}
			if( keyEnd < keyStart )
			{
				idLib::FatalError( "%s File ended while reading key at line %d", name, line );
			}
			tempKey.CopyRange( ( char* )buffer, keyStart, keyEnd );
			
			int valStart = -1;
			while( i < bufferLen )
			{
				c = buffer[i++];
				if( c == '\"' )
				{
					valStart = i;
					break;
				}
			}
			if( valStart < 0 )
			{
				idLib::FatalError( "%s File ended while reading value at line %d", name, line );
			}
			int valEnd = -1;
			tempVal.CapLength( 0 );
			while( i < bufferLen )
			{
				c = utf8 ? idStr::UTF8Char( buffer, i ) : buffer[i++];
				if( !utf8 && c >= 0x80 )
				{
					// this is a serious error and we must check this to avoid accidentally shipping a file where someone squased UTF-8 encodings
					idLib::FatalError( "Language file %s is supposed to be plain ASCII, but has byte values > 127!", name );
				}
				if( c == '\"' )
				{
					valEnd = i - 1;
					continue;
				}
				if( c == '\n' )
				{
					line++;
					break;
				}
				if( c == '\r' )
				{
					continue;
				}
				if( c == '\\' )
				{
					c = utf8 ? idStr::UTF8Char( buffer, i ) : buffer[i++];
					if( c == 'n' )
					{
						c = '\n';
					}
					else if( c == 't' )
					{
						c = '\t';
					}
					else if( c == '\"' )
					{
						c = '\"';
					}
					else if( c == '\\' )
					{
						c = '\\';
					}
					else
					{
						idLib::Warning( "Unknown escape sequence %x at line %d", c, line );
					}
				}
				tempVal.AppendUTF8Char( c );
			}
			if( valEnd < valStart )
			{
				idLib::FatalError( "%s File ended while reading value at line %d", name, line );
			}
			if( lang_maskLocalizedStrings.GetBool() && tempVal.Length() > 0 && tempKey.Find( "#font_" ) == -1 )
			{
				int len = tempVal.Length();
				if( len > 0 )
				{
					tempVal.Fill( 'W', len - 1 );
				}
				else
				{
					tempVal.Empty();
				}
				tempVal.Append( 'X' );
			}
			AddKeyVal( tempKey, tempVal );
			numStrings++;
		}
	}
	
	idLib::Printf( "%i strings read\n", numStrings );
	
	// get rid of any waste due to geometric list growth
	//mem.PushHeap();
	keyVals.Condense();
	//mem.PopHeap();
	
	return true;
}

/*
========================
idLangDict::Save
========================
*/
bool idLangDict::Save( const char* fileName )
{
	idFile* outFile = fileSystem->OpenFileWrite( fileName );
	if( outFile == NULL )
	{
		idLib::Warning( "Error saving: %s", fileName );
		return false;
	}
	byte bof[3] = { 0xEF, 0xBB, 0xBF };
	outFile->Write( bof, 3 );
	outFile->WriteFloatString( "// string table\n//\n\n{\n" );
	for( int j = 0; j < keyVals.Num(); j++ )
	{
		const idLangKeyValue& kvp = keyVals[j];
		if( kvp.value == NULL )
		{
			continue;
		}
		outFile->WriteFloatString( "\t\"%s\"\t\"", kvp.key );
		for( int k = 0; kvp.value[k] != 0; k++ )
		{
			char ch = kvp.value[k];
			if( ch == '\t' )
			{
				outFile->Write( "\\t", 2 );
			}
			else if( ch == '\n' || ch == '\r' )
			{
				outFile->Write( "\\n", 2 );
			}
			else if( ch == '"' )
			{
				outFile->Write( "\\\"", 2 );
			}
			else if( ch == '\\' )
			{
				outFile->Write( "\\\\", 2 );
			}
			else
			{
				outFile->Write( &ch, 1 );
			}
		}
		outFile->WriteFloatString( "\"\n" );
	}
	outFile->WriteFloatString( "\n}\n" );
	delete outFile;
	return true;
}

/*
========================
idLangDict::GetString
========================
*/
const char* idLangDict::GetString( const char* str ) const
{
	const char* localized = FindString( str );
	if( localized == NULL )
	{
		return str;
	}
	return localized;
}

/*
========================
idLangDict::FindStringIndex
========================
*/
int idLangDict::FindStringIndex( const char* str ) const
{
	if( str == NULL )
	{
		return -1;
	}
	int hash = idStr::IHash( str );
	for( int i = keyIndex.GetFirst( hash ); i >= 0; i = keyIndex.GetNext( i ) )
	{
		if( idStr::Icmp( str, keyVals[i].key ) == 0 )
		{
			return i;
		}
	}
	return -1;
}

/*
========================
idLangDict::FindString_r
========================
*/
const char* idLangDict::FindString_r( const char* str, int& depth ) const
{
	depth++;
	if( depth > MAX_REDIRECTION_DEPTH )
	{
		// This isn't an error because we assume the error will be obvious somewhere in a GUI or something,
		// and the whole point of tracking the depth is to avoid a crash.
		idLib::Warning( "String '%s', indirection depth > %d", str, MAX_REDIRECTION_DEPTH );
		return NULL;
	}
	
	if( str == NULL || str[0] == '\0' )
	{
		return NULL;
	}
	
	int index = FindStringIndex( str );
	if( index < 0 )
	{
		return NULL;
	}
	const char* value = keyVals[index].value;
	if( value == NULL )
	{
		return NULL;
	}
	if( IsStringId( value ) )
	{
		// this string is re-directed to another entry
		return FindString_r( value, depth );
	}
	return value;
}

/*
========================
idLangDict::FindString
========================
*/
const char* idLangDict::FindString( const char* str ) const
{
	int depth = 0;
	return FindString_r( str, depth );
}

/*
========================
idLangDict::DeleteString
========================
*/
bool idLangDict::DeleteString( const char* key )
{
	return DeleteString( FindStringIndex( key ) );
}

/*
========================
idLangDict::DeleteString
========================
*/
bool idLangDict::DeleteString( const int idx )
{
	if( idx < 0 || idx >= keyVals.Num() )
	{
		return false;
	}
	
	//mem.PushHeap();
	blockAlloc.Free( keyVals[idx].value );
	keyVals[idx].value = NULL;
	//mem.PopHeap();
	
	return true;
}

/*
========================
idLangDict::RenameStringKey
========================
*/
bool idLangDict::RenameStringKey( const char* oldKey, const char* newKey )
{
	int index = FindStringIndex( oldKey );
	if( index < 0 )
	{
		return false;
	}
	//mem.PushHeap();
	blockAlloc.Free( keyVals[index].key );
	int newKeyLen = idStr::Length( newKey );
	keyVals[index].key = blockAlloc.Alloc( newKeyLen + 1 );
	idStr::Copynz( keyVals[index].key, newKey, newKeyLen + 1 );
	int oldHash = idStr::IHash( oldKey );
	int newHash = idStr::IHash( newKey );
	if( oldHash != newHash )
	{
		keyIndex.Remove( oldHash, index );
		keyIndex.Add( newHash, index );
	}
	//mem.PopHeap();
	
	return true;
}

/*
========================
idLangDict::SetString
========================
*/
bool idLangDict::SetString( const char* key, const char* val )
{
	int index = FindStringIndex( key );
	if( index < 0 )
	{
		return false;
	}
	//mem.PushHeap();
	if( keyVals[index].value != NULL )
	{
		blockAlloc.Free( keyVals[index].value );
	}
	int valLen = idStr::Length( val );
	keyVals[index].value = blockAlloc.Alloc( valLen + 1 );
	idStr::Copynz( keyVals[index].value, val, valLen + 1 );
	//mem.PopHeap();
	return true;
}

/*
========================
idLangDict::AddKeyVal
========================
*/
void idLangDict::AddKeyVal( const char* key, const char* val )
{
	if( SetString( key, val ) )
	{
		return;
	}
	//mem.PushHeap();
	int keyLen = idStr::Length( key );
	char* k = blockAlloc.Alloc( keyLen + 1 );
	idStr::Copynz( k, key, keyLen + 1 );
	char* v = NULL;
	if( val != NULL )
	{
		int valLen = idStr::Length( val );
		v = blockAlloc.Alloc( valLen + 1 );
		idStr::Copynz( v, val, valLen + 1 );
	}
	int index = keyVals.Append( idLangKeyValue( k, v ) );
	int hash = idStr::IHash( key );
	keyIndex.Add( hash, index );
	//mem.PopHeap();
}

/*
========================
idLangDict::AddString
========================
*/
const char* idLangDict::AddString( const char* val )
{
	int i = Sys_Milliseconds();
	idStr key;
	sprintf( key, "#str_%06d", ( i++ % 1000000 ) );
	while( FindStringIndex( key ) > 0 )
	{
		sprintf( key, "#str_%06d", ( i++ % 1000000 ) );
	}
	AddKeyVal( key, val );
	int index = FindStringIndex( key );
	return keyVals[index].key;
}

/*
========================
idLangDict::GetNumKeyVals
========================
*/
int idLangDict::GetNumKeyVals() const
{
	return keyVals.Num();
}

/*
========================
idLangDict::GetKeyVal
========================
*/
const idLangKeyValue* idLangDict::GetKeyVal( int i ) const
{
	return &keyVals[i];
}

/*
========================
idLangDict::IsStringId
========================
*/
bool idLangDict::IsStringId( const char* str )
{
	return idStr::Icmpn( str, KEY_PREFIX, KEY_PREFIX_LEN ) == 0;
}

/*
========================
idLangDict::GetLocalizedString
========================
*/
const char* idLangDict::GetLocalizedString( const idStrId& strId ) const
{
	if( strId.GetIndex() >= 0 && strId.GetIndex() < keyVals.Num() )
	{
		if( keyVals[ strId.GetIndex() ].value == NULL )
		{
			return keyVals[ strId.GetIndex() ].key;
		}
		else
		{
			return keyVals[ strId.GetIndex() ].value;
		}
	}
	return "";
}

/*
================================================================================================
idStrId
================================================================================================
*/

/*
========================
idStrId::Set
========================
*/
void idStrId::Set( const char* key )
{
	if( key == NULL || key[0] == 0 )
	{
		index = -1;
	}
	else
	{
		index = idLocalization::languageDict.FindStringIndex( key );
		if( index < 0 )
		{
			// don't allow setting of string ID's to an unknown ID... this should only be allowed from
			// the string table tool because additions from anywhere else are not guaranteed to be
			// saved to the .lang file.
			idLib::Warning( "Attempted to set unknown string ID '%s'", key );
		}
	}
}

/*
========================
idStrId::GetKey
========================
*/
const char* idStrId::GetKey() const
{
	if( index >= 0 && index < idLocalization::languageDict.keyVals.Num() )
	{
		return idLocalization::languageDict.keyVals[index].key;
	}
	return "";
}

/*
========================
idStrId::GetLocalizedString
========================
*/
const char* idStrId::GetLocalizedString() const
{
	return idLocalization::languageDict.GetLocalizedString( *this );
}