/*
Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/

importPackage( java.util.regex );

var CKLANGTOOL =
{
	isCompiled : false,
	languageDir : "",
	templateFile : "",
	format : "json",
	/**
	 * Holds the content of en.js language file where strings are replaced with
	 * special placeholders: #ckeditor_translation.placeholder.key#.
	 */
	template : "",
	path : "",
	
	load : function( className )
	{
		if ( CKLANGTOOL.isCompiled )
		{
			loadClass( className );
		}
		else
		{
			var path = className;
			path = path.replace( /^cklangtool\./, "_source/" );
			path = path.replace( /\./g, '/' ) + '.js';

			load( CKLANGTOOL.path + path );
		}
	}	
};

( function()
{
	function stripExtension( fileName )
	{
		var pos = fileName.lastIndexOf( "." );
		if ( pos == -1 )
			return "";
		else
			return String( fileName.substring( 0, pos ).toLowerCase() );
	}

	/**
	 * Load language file and return an object with the whole translation.
	 */
	CKLANGTOOL.loadLanguageFile = function( file )
	{
		var format = this.theFileFormat( file );
		switch( format )
		{
			case CKLANGTOOL.FORMAT_JSON:
				var translationCode = 'var CKEDITOR = { lang : {}, plugins : { setLang : function(plugin, langCode, obj) { CKEDITOR.lang[langCode] = obj; } } }; ' + CKLANGTOOL.io.readFile( file );

				var cx = Context.enter(), scope = cx.initStandardObjects();

				try
				{
					cx.evaluateString( scope, translationCode, file.getName(), 1, null );

					var languageCode = '';

					/*
					 * Get the number of variables in parent scope.
					 */
					var size = 0;
					for ( var i in scope )
					{
						size++;
					}

					/*
					 * If there is more than one variable, then it's not a CKEDITOR language file.
					 */
					if ( size > 1 )
					{
						/**
						 * Return the first variable from parent scope different than
						 * CKEDITOR.
						 */
						for ( var i in scope )
						{
							if ( i != "CKEDITOR" )
								return { 'languageCode' : languageCode, 'translation' : scope[i] };
						}
					}
					else
					{
						/*
						 * Return the first entry from scope.CKEDITOR.lang object
						 */
						for ( var i in scope.CKEDITOR.lang )
						{
							languageCode = i;
							return { 'languageCode' : languageCode, 'translation' : scope.CKEDITOR.lang[i] };
						}
					}
				}
				catch ( e )
				{
					throw ( "Language file is invalid: " + file.getAbsolutePath() + ".\nError: " + e.message);
				}

			case CKLANGTOOL.FORMAT_GT:

				function msgstr( val )
				{
					// [ 'msgid-plural', 'trans', 'trans-plural' ... ]
					var str = val[ 1 ];

					// Restore line breaks.
					return str ? str.replace( /\\n/g, '\r\n' ) : '';
				}

				var pom = parsePO( CKLANGTOOL.io.readFile( file ) ),
					headers = pom[ '' ],
					languageCode = headers.language;

				if ( languageCode.toLowerCase() != stripExtension(file.getName()).toLowerCase() )
				{
					throw ( "Language file does not seem to be valid.\n" +
							"Language code (" + languageCode + ") and the name of the file (" + file.getName()+ ") do not match.\n" +
							"Is the following file valid: " + file.getAbsolutePath() + " ?\n" );
				}

				var trans = {}, translated, keys, key, entry;
				for ( var msgid in pom )
				{
					translated = msgstr( pom[ msgid ]);
					// Bypass file header and untranslated ones.
					if ( msgid && translated )
					{
						keys = msgid.split( '.' );
						entry = trans;

						for ( var i = 0, level = keys.length; i < level; i++ )
						{
							key = keys[ i ];
							if ( !entry[ key ] )
							{
								if ( i < level - 1 )
									entry = entry[ key ] = {};
								else
									entry[ key ] = translated;
							}
							else if ( typeof entry[ key ] == 'object' )
								entry = entry[ key ];
						}
					}
				}

				return { 'languageCode' : languageCode, 'translation' : trans };

			default:
				throw 'Load translation of the <' + format + '> format is not supported';
		}
	};

	CKLANGTOOL.translator = function()
	{
		CKLANGTOOL.englishTranslation =
		{};
	};

	/**
	 * Language code of currently processed file (taken from
	 * CKEDITOR.lang['code']).
	 */
	var languageCode;
	var fileOverviewBlock;

	/**
	 * Check whether the javascript file is valid.
	 */
	function checkFile( file )
	{
		var format = CKLANGTOOL.theFileFormat( file );
		if ( format == CKLANGTOOL.FORMAT_JSON )
		{
			var compilerEnv = new CompilerEnvirons();
			var errorReporter = compilerEnv.getErrorReporter();
			var parser = new Parser( compilerEnv, errorReporter );

			try
			{
				parser.parse( 'var CKEDITOR = { lang : {} }; ' + CKLANGTOOL.io.readFile( file ), null, 1 );
				return false;
			}
			catch ( e )
			{
				throw ("Error in " + file.getAbsolutePath() + "\n" + "Line: " + e.lineNumber + "\nMessage: " + e.message);
			}
		}
		else
			return;
	}

	( function()
	{
		CKLANGTOOL.FORMAT_JSON = 'json';
		CKLANGTOOL.FORMAT_JP = 'jprops';
		CKLANGTOOL.FORMAT_GT = 'gettext';

		function extToFormat( ext )
		{
			var format =
			{
				'js': CKLANGTOOL.FORMAT_JSON,
				'po' : CKLANGTOOL.FORMAT_GT,
				'properties': CKLANGTOOL.FORMAT_JP
			}[ ext ];

			return format || 'unknown';
		}

		CKLANGTOOL.theFileFormat = function( file )
		{
			var ext = CKLANGTOOL.io.getExtension( file.getName() )
			return extToFormat( ext );
		};

	} )();


	var regexLib =
	{
		inlineComment :Pattern.compile( "^\\s*\\/\\/" ),
		missing :Pattern.compile( "\\/\\/\\s*MISSING", Pattern.CASE_INSENSITIVE ),
		blockCommentStart :Pattern.compile( "\\/\\*" ),
		blockCommentEnd :Pattern.compile( "\\*\\/" ),
		entry :Pattern.compile( "^(\\s*)((?:'|\")?)([a-z0-9][_a-z0-9]*)\\2(\\s*:\\s*\\').*?(\\'.*)$", Pattern.CASE_INSENSITIVE ),
		arrayEntry :Pattern.compile( "^(\\s*)((?:'|\")?)([a-z0-9][_a-z0-9]*)\\2(\\s*:\\s*\\[)(.*?)(\\].*)$", Pattern.CASE_INSENSITIVE ),
		arrayItemEntry :Pattern.compile( "\\s*(?:'(.*?)'(?:\\s*,\\s*)?)" ),
		arrayTranslationKey :Pattern.compile( "^(.*)\\[(\\d+)\\]$" ),
		objectName :Pattern.compile( "^\\s*((?:'|\")?)([a-z][_a-z0-9]*)\\1\\s*:\\s*(?://.*)?$", Pattern.CASE_INSENSITIVE ),
		objectStart :Pattern.compile( "\\{" ),
		objectEnd :Pattern.compile( "\\}" ),
		fileOverview :Pattern.compile( " @fileOverview" ),
		translation :Pattern.compile( "#ckeditor_translation[^#]*?#" ),
		ckeditorLang :Pattern.compile( "(.*CKEDITOR\\.lang\\[).*?(\\]\\s*=.*)" ),
		ckeditorSetLang :Pattern.compile( "(.*CKEDITOR\\.plugins\\.setLang\\s*\\(.*?,)\\s*['\"][a-z-]+['\"](.*)" )
	};

	/**
	 * Returns an array with missing keys (already marked as //MISSING).
	 */
	CKLANGTOOL.analyzeLanguageFile = function( file )
	{
		var fileOverview = '/**\n* @fileOverview \n*/';
		var key = "ckeditor_translation";
		var out =
		{};
		var inBlockComment = false;
		var blockComment = [];
		var objectName, matcher, line, translationKey;
		var lines = CKLANGTOOL.io.readFileIntoArray( file );

		for ( var j = 0 ; j < lines.length ; j++ )
		{
			line = lines[ j ];
			if ( !inBlockComment )
			{
				matcher = regexLib.inlineComment.matcher( line );
				if ( matcher.find() )
				{
					continue;
				}

				matcher = regexLib.blockCommentStart.matcher( line );
				if ( matcher.find() )
				{
					inBlockComment = true;
					blockComment.push( line );
					continue;
				}

				matcher = regexLib.objectName.matcher( line );
				if ( matcher.find() )
				{
					objectName = matcher.group( 2 );
					continue;
				}

				if ( objectName )
				{
					matcher = regexLib.objectStart.matcher( line );
					/*
					 * We have found an opening bracket, key -> key.objectName
					 */
					if ( matcher.find() )
					{
						key = key + "." + objectName;
						continue;
					}

					matcher = regexLib.objectEnd.matcher( line );
					/*
					 * We have found a closing bracket, key.objectName -> key
					 */
					if ( matcher.find() )
					{
						if ( key.indexOf( "." ) != -1 )
							key = key.slice( 0, key.lastIndexOf( "." ) );
						continue;
					}
				}

				/*
				 * Get rid of all escaped quotes, we don't need the exact content at this stage, just the key
				 */
				matcher = regexLib.entry.matcher( line.replaceAll( "\\\\'", "" ) );
				if ( matcher.find() && regexLib.missing.matcher( line ).find() )
				{
					translationKey = key + "." + matcher.group( 3 );
					translationKey = translationKey.replace( /^ckeditor_translation\./, "" );
					out[ translationKey ] = true;
				}

				/*
				 * Get rid of all escaped quotes, we don't need the exact content at this stage, just the key.
				 */
				matcher = regexLib.arrayEntry.matcher( line.replaceAll( "\\\\'", "" ) );
				if ( matcher.find() && regexLib.missing.matcher( line ).find() )
				{
					translationKey = key + "." + matcher.group( 3 );
					translationKey = translationKey.replace( /^ckeditor_translation\./, "" );
					out[ translationKey ] = true;
				}
			}
			else
			{
				blockComment.push( line );

				matcher = regexLib.blockCommentEnd.matcher( line );
				if ( matcher.find() )
				{
					inBlockComment = false;

					matcher = regexLib.fileOverview.matcher( blockComment.join( "" ) );
					if ( matcher.find() )
					{
						fileOverview = blockComment.join( "\n" );
					}
					blockComment = [];
				}
			}
		}

		return { missingKeys : out, fileOverviewBlock : fileOverview };
	};

	/**
	 * Creates template from the english language file.
	 * 
	 * All strings are replaced with placeholders:
	 * #ckeditor_translation.translationKey#
	 * 
	 * There are also two special placeholders:
	 * #ckeditor_translation.__languageCode# (language code)
	 * 
	 * #ckeditor_translation.__fileOverview# (the block comment with the file
	 * description)
	 */
	CKLANGTOOL.translator.createTemplate = function( file )
	{
		var key = "ckeditor_translation";
		var out = [];
		var inBlockComment = false;
		var blockComment = [];
		var i, matcher, matchResult, objectName, string, line;
		var arrayEntryItems, arrayEntryItemsMatcher, arrayEntryLineEnd, arrayEntryLine, arrayEntryKey;
		var lines = CKLANGTOOL.io.readFileIntoArray( file );

		for ( var j = 0 ; j < lines.length ; j++ )
		{
			line = lines[ j ];

			if ( !inBlockComment )
			{
				matcher = regexLib.inlineComment.matcher( line );
				if ( matcher.find() )
				{
					out.push( line );
					continue;
				}

				matcher = regexLib.blockCommentStart.matcher( line );
				if ( matcher.find() )
				{
					inBlockComment = true;
					blockComment.push( line );
					continue;
				}

				matcher = regexLib.objectName.matcher( line );
				if ( matcher.find() )
				{
					objectName = matcher.group( 2 );
					out.push( line );
					continue;
				}

				if ( objectName )
				{
					matcher = regexLib.objectStart.matcher( line );
					/*
					 * We have found an opening bracket, key -> key.objectName
					 */
					if ( matcher.find() )
					{
						key = key + "." + objectName;
						out.push( line );
						continue;
					}

					matcher = regexLib.objectEnd.matcher( line );
					/*
					 * We have found a closing bracket, key.objectName -> key
					 */
					if ( matcher.find() )
					{
						key = key.slice( 0, key.lastIndexOf( "." ) );
						out.push( line );
						continue;
					}
				}

				/* 
				 * Find CKEDITOR.lang['en']
				 */
				matcher = regexLib.ckeditorLang.matcher( line );
				if ( matcher.find() )
				{
					out.push( matcher.group( 1 ) + "'#ckeditor_translation.__languageCode#'" + matcher.group( 2 ) );
					continue;
				}

				/*
				 * CKEDITOR.plugins.setLang( 'uicolor', 'en',
				 */
				matcher = regexLib.ckeditorSetLang.matcher( line );
				if ( matcher.find() )
				{
					out.push( matcher.group( 1 ) + "'#ckeditor_translation.__languageCode#'" + matcher.group( 2 ) );
					continue;
				}

				/* 
				 * Get rid of all escaped quotes, we don't need the exact content at this stage, just the key.
				 * We're changing here the entry into the key.
				 * So 'Upload' becomes '#ckeditor_translation.Upload#' in our temporary template.  
				 */
				matcher = regexLib.entry.matcher( line.replaceAll( "\\\\'", "" ) );
				if ( matcher.find() )
				{
					out.push( matcher.group( 1 ) + matcher.group( 2 ) + matcher.group( 3 ) + matcher.group( 2 ) + matcher.group( 4 ) + "#" + key + "." + matcher.group( 3 ) + "#"
							+ matcher.group( 5 ) );
					continue;
				}

				/* 
				 * Get rid of all escaped quotes, we don't need the exact content at this stage, just the key.
				 * We're changing here the entry into the key.
				 * So ['AM', 'PM'] becomes 
				 * ['#ckeditor_translation.DateAmPm[0]#', '#ckeditor_translation.DateAmPm[1]#'] 
				 * in our temporary template.  
				 */
				matcher = regexLib.arrayEntry.matcher( line.replaceAll( "\\\\'", "" ) );
				if ( matcher.find() )
				{
					i = 0;

					arrayEntryLine = matcher.group( 1 ) + matcher.group( 2 ) + matcher.group( 3 ) + matcher.group( 2 ) + matcher.group( 4 );
					arrayEntryKey = matcher.group( 3 );
					arrayEntryLineEnd = matcher.group( 6 );
					arrayEntryItems = matcher.group( 5 );

					arrayEntryItemsMatcher = regexLib.arrayItemEntry.matcher( arrayEntryItems );
					while ( arrayEntryItemsMatcher.find() )
					{
						matchResult = arrayEntryItemsMatcher.toMatchResult();
						if ( i > 0 )
						{
							arrayEntryLine += ", ";
						}
						arrayEntryLine += "'#" + key + "." + arrayEntryKey + "[" + i + "]" + "#'";
						i++;
					}
					arrayEntryLine += arrayEntryLineEnd;
					out.push( arrayEntryLine );
					continue;
				}

				out.push( line );
			}
			else
			{
				blockComment.push( line );

				matcher = regexLib.blockCommentEnd.matcher( line );
				if ( matcher.find() )
				{
					inBlockComment = false;

					matcher = regexLib.fileOverview.matcher( blockComment.join( "" ) );
					/**
					 * Add a placeholder for the fileOverview section.
					 */
					if ( matcher.find() )
					{
						out.push( "#ckeditor_translation.__fileOverview#" );
					}
					else
					{
						out.push( blockComment.join( "\n" ) );
					}
					blockComment = [];
				}
			}
		}

		/**
		 * Uncomment this line to see the template.
		 */
		// CKLANGTOOL.io.saveFile( new File( CKLANGTOOL.languageDir, "template.txt" ), out.join( "\r\n" ), false );
		return out.join( "\n" ).replace(/\s*$/, "\n");
	}

	/**
	 * Return translation[translationKey].
	 * 
	 * If translation contains dots, for example: common.textField then return
	 * translation[common][textfield]
	 */
	CKLANGTOOL.translator.getTranslation = function( translation, translationKey )
	{
		var dotPos;
		var result = translation;

		/**
		 * Special case, return the language code of processed file.
		 */
		if ( translationKey == "__languageCode" )
			return languageCode;

		/**
		 * Special case, return the fileOverview block of processed file.
		 */
		if ( translationKey == "__fileOverview" )
			return fileOverviewBlock;

		while ( (dotPos = translationKey.indexOf( "." )) != -1 )
		{
			result = result[ translationKey.substring( 0, dotPos ) ];
			if ( typeof (result) == "undefined" )
			{
				return false;
			}
			translationKey = translationKey.slice( dotPos + 1 );
		}

		/*
		 * First make sure that the translationKey is not an array.
		 */
		var matcher = regexLib.arrayTranslationKey.matcher( translationKey );
		if ( matcher.find() )
		{
			if ( typeof (result[ matcher.group( 1 ) ]) != "object" )
			{
				return false;
			}
			result = result[ matcher.group( 1 ) ][ matcher.group( 2 ) ];
		}
		else
		{
			result = result[ translationKey ];
		}

		if ( typeof (result) == "undefined" )
		{
			return false;
		}

		return escapeString( result );
	}

	/*
	 * Escapes all characters so that a string could be properly saved to a file.
	 */
	function escapeString( string )
	{
		return string.replace( /\\/g, "\\\\" ).replace( /\r/g, "\\r" ).replace( /\n/g, "\\n" ).replace( /'/g, "\\'" ).replace( /\u200b/g,
				"\\u200b" );
	}

	function processFile( file )
	{
		var matchResult, replacement, translationKey;
		var string = CKLANGTOOL.template;
		var matcher = regexLib.translation.matcher( string );
		var found = 0, missing = 0;
		var result = CKLANGTOOL.loadLanguageFile( file );
		var translation = result.translation;
		languageCode = result.languageCode;

		var dst = new File( CKLANGTOOL.languageDir, languageCode + '.js' );
		var info = CKLANGTOOL.analyzeLanguageFile( dst );
		// No mark missing support in other formats.
		var missingKeys =  CKLANGTOOL.format == CKLANGTOOL.FORMAT_JSON ? info.missingKeys : {};
		fileOverviewBlock = info.fileOverviewBlock;


		while ( matcher.find() )
		{
			matchResult = matcher.toMatchResult();
			/*
			 * #ckeditor_translation.common.textField# -> common.textField
			 */
			translationKey = matchResult.group( 0 ).slice( 22, -1 );
			replacement = CKLANGTOOL.translator.getTranslation( translation, translationKey );

			/*
			 * common.textField[1] -> common.textField
			 */
			if ( replacement == false || missingKeys[ translationKey.replace( /\[\d+\]$/, "" ) ] )
			{
				/**
				 * FoldersTitle : '#ckeditor_translation.FoldersTitle#', ->
				 * FoldersTitle : '[MISSING_TRANSLATION]Folders',
				 */
				replacement = "[MISSING_TRANSLATION]" + CKLANGTOOL.translator.getTranslation( CKLANGTOOL.englishTranslation, translationKey );
				string = (string.substring( 0, matchResult.start() ) + replacement + string.substring( matchResult.end() ));

				if ( translationKey.substring( 0, 2 ) != "__" )
					missing++;
			}
			else
			{
				string = (string.substring( 0, matchResult.start() ) + replacement + string.substring( matchResult.end() ));

				if ( translationKey.substring( 0, 2 ) != "__" )
					found++;
			}

			matcher.reset( string );
		}

		/**
		 * Loop through all lines, remove [MISSING_TRANSLATION] and add
		 * //MISSING comment at the end of line (if necessary).
		 */
		var line, lines = string.split( "\n" );
		for ( var i = 0 ; i < lines.length ; i++ )
		{
			line = lines[ i ];
			if ( line.indexOf( "[MISSING_TRANSLATION]" ) != -1 )
			{
				if ( line.indexOf( "//" ) == -1 )
				{
					lines[ i ] = line.replace( /\[MISSING_TRANSLATION\]/g, '' ) + " // MISSING";
				}
				else
				{
					lines[ i ] = line.replace( "//", "// MISSING //" ).replace( /\[MISSING_TRANSLATION\]/g, '' );
				}
			}
		}

		CKLANGTOOL.io.saveFile( dst, lines.join( "\r\n" ), false );

		var result =
		{
			langFile : dst.getName(),
			found :found,
			missing :missing
		};

		return result;
	}

	function padRight( value, length )
	{
		return value + Array( length - value.length() ).join( " " );
	}

	CKLANGTOOL.translator.prototype =
	{
		run : function( transDir )
		{
			CKLANGTOOL.template = CKLANGTOOL.translator.createTemplate( CKLANGTOOL.templateFile );
			var result = CKLANGTOOL.loadLanguageFile( CKLANGTOOL.templateFile );
			CKLANGTOOL.englishTranslation = result.translation; 

			var children = transDir.list();
			var file, languages = [], status = {};
			var foundFiles = false;

			for ( var i = 0 ; i < children.length ; i++ )
			{
				/**
				 * Skip files beginning with underscore character.
				 */
				if ( children[ i ].charAt(0) == 95 )
					continue;

				// Bypass source language file.
				if ( children[ i ].match( /^en\./ ) )
					continue;

				file = new File( transDir, children[ i ] );

				// Bypass directory and irrelevant files.
				if ( ! ( file.isFile() && CKLANGTOOL.theFileFormat( file ) == CKLANGTOOL.format ) )
					continue;

				print( "Processing " + file.getAbsolutePath() );
				result = processFile( file );
				checkFile( file );

				languages.push( result.langFile );
				status[ result.langFile ] = padRight( result.langFile, 12 ) + "Found: " + result.found + " Missing: " + result.missing;
				foundFiles = true;
			}

			if ( !foundFiles )
			{
				print( "WARNING: language files not found." );
			}

			var date = new Date(),
				header = "Copyright (c) 2003-" + date.getFullYear() + ", CKSource - Frederico Knabben. All rights reserved.\r\nFor licensing, see LICENSE.html or http://ckeditor.com/license\r\n\r\n";

			languages.sort();
			var output = [];
			for ( i = 0 ; i < languages.length ; i++)
			{
				output.push( status[ languages[ i ] ] );
			}

			CKLANGTOOL.io.saveFile( new File( CKLANGTOOL.languageDir, "_translationstatus.txt" ), header + output.join( "\r\n" ) + "\r\n", false );
			print( "Process completed." );
		}
	};
})();
