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

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 );
}

