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

importPackage( org.mozilla.javascript );
importPackage( java.util.regex );

importClass( java.io.File );
importClass( java.io.FileReader );

CKPACKAGER.load( 'ckpackager.includes.scope' );
CKPACKAGER.load( 'ckpackager.includes.token' );

(function()
{
	var scope,
		tree,
		noGlobals,
		constantList,
		lastWasVar,
		ignoreSiblings,
		isLoop,
		output,
		outputSize = 0,
		maxSize = 2500;

	var lang = {};

	lang[ Token.NOT ] = '!';
	lang[ Token.BITNOT ] = '~';
	lang[ Token.POS ] = '+';
	lang[ Token.NEG ] = '-';
	lang[ Token.TYPEOF ] = 'typeof ';
	lang[ Token.MUL ] = '*';
	lang[ Token.DIV ] = '/';
	lang[ Token.MOD ] = '%';
	lang[ Token.ADD ] = '+';
	lang[ Token.SUB ] = '-';
	lang[ Token.RSH ] = '>>';
	lang[ Token.LSH ] = '<<';
	lang[ Token.URSH ] = '>>>';
	lang[ Token.LT ] = '<';
	lang[ Token.LE ] = '<=';
	lang[ Token.GT ] = '>';
	lang[ Token.GE ] = '>=';
	lang[ Token.EQ ] = '==';
	lang[ Token.NE ] = '!=';
	lang[ Token.SHEQ ] = '===';
	lang[ Token.SHNE ] = '!==';
	lang[ Token.BITAND ] = '&';
	lang[ Token.BITXOR ] = '^';
	lang[ Token.BITOR ] = '|';
	lang[ Token.AND ] = '&&';
	lang[ Token.OR ] = '||';
	lang[ Token.COMMA ] = ',';
	lang[ Token.IN ] = ' in ';

	// From https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Operators/Operator_Precedence
	var precedence = {};
	precedence[ Token.DOT ] = 1;
	precedence[ Token.INC ] = precedence[ Token.DEC ] = 3;
	precedence[ Token.NOT ] = precedence[ Token.BITNOT ] = precedence[ Token.POS ] = precedence[ Token.NEG ] = precedence[ Token.TYPEOF ] = 4;
	precedence[ Token.MUL ] = precedence[ Token.DIV ] = precedence[ Token.MOD ] = 5;
	precedence[ Token.ADD ] = precedence[ Token.SUB ] = 6;
	precedence[ Token.RSH ] = precedence[ Token.LSH ] = precedence[ Token.URSH ] = 7;
	precedence[ Token.LT ] = precedence[ Token.LE ] = precedence[ Token.GT ] = precedence[ Token.GE ] = precedence[ Token.INSTANCEOF ] = precedence[ Token.IN ] = 8;
	precedence[ Token.EQ ] = precedence[ Token.NE ] = precedence[ Token.SHEQ ] = precedence[ Token.SHNE ] = 9;
	precedence[ Token.BITAND ] = 10;
	precedence[ Token.BITXOR ] = 11;
	precedence[ Token.BITOR ] = 12;
	precedence[ Token.AND ] = 13;
	precedence[ Token.OR ] = 14;
	precedence[ Token.HOOK ] = 15;
	precedence[ Token.SETNAME ] = precedence[ Token.SETPROP ] = precedence[ Token.SETELEM ] = 16;
	precedence[ Token.COMMA ] = 17;

	// Tokens having right-to-left associativity.
	// From https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Operators/Operator_Precedence
	var associativityRTL = {};
	associativityRTL[ Token.SETNAME ] =
	associativityRTL[ Token.SETPROP ] =
	associativityRTL[ Token.SETELEM ] =
	associativityRTL[ Token.HOOK ] =
	associativityRTL[ Token.TYPEOF ] =
	associativityRTL[ Token.NEG ] =
	associativityRTL[ Token.POS ] =
	associativityRTL[ Token.NOT ] = 1;

	var noAssociativity = {};
	noAssociativity[ Token.AND ] =
	noAssociativity[ Token.OR ] = 1;

	//Other
	precedence[ Token.FUNCTION ] = 100;

	var noSpaceChars = {' ':1,';':1,',':1,'.':1,'(':1,')':1,'[':1,']':1,'{':1,'}':1,'?':1,':':1,'!':1,'=':1,'>':1,'<':1,'*':1,'/':1,'%':1,'+':1,'-':1,'&':1,'|':1,'^':1};
	var out = function()
	{
		if ( out.checkSpace )
		{
			var first = String( arguments[ 0 ] );
			first = first.substr( 0, 1 );

			if (
				( first == '+' && out.last == '+' ) ||
				( first == '-' && out.last == '-' ) ||
				( out.mayNeedSpace && !noSpaceChars[ first ] && !noSpaceChars[ out.last ] ) )
			{
				output.push( ' ' );
				outputSize++;
			}
		}

		var last = String( arguments[ arguments.length - 1 ] );
		last = out.last = last.substr( last.length - 1 );
		out.mayNeedSpace = ( last == '+' || last == '-' || !noSpaceChars[ last ] );

		for ( var i = 0 ; i < arguments.length ; i++ )
		{
			var str = String( arguments[i] );
			outputSize += str.length;
			output.push( str );
		}

		if ( outputSize > maxSize && last == ';' )
		{
			output.push( '\n' );
			outputSize = 0;
		}
	};

	out.mayNeedSpace = false;
	out.checkSpace = true;
	out.size = 0;

	var outString = function( str )
	{
		var value = String( str ),
			singleParts = value.split( "'" ),
			doubleParts = value.split( '"' );

		value = value.replace( /[\b\t\n\v\f\r\x1b\xa0\ufeff\\]/g, function( match )
			{
				var chars =
				{
					'\b' : '\\b',
					'\t' : '\\t',
					'\n' : '\\n',
					'\v' : '\\v',
					'\f' : '\\f',
					'\r' : '\\r',
					'\\' : '\\\\',
					'\x1b' : '\\x1b',
					'\xa0' : '\\xa0',
					'\ufeff' : '\\ufeff'
				};

				return chars[ match ];
			} );

		if ( doubleParts.length > singleParts.length )
			out( "'", value.replace( /'/g, "\\'" ), "'" );
		else if ( singleParts.length > 1 )
			out( '"', value.replace( /"/g, '\\"' ), '"' );
		else
			out( "'", value, "'" );
	};

	var isReserved = function( word )
	{
		return !!isReserved.words[ word ];
	};
	// Taken from the ECMA-262.
	// Additionally, "null" is reserved as null literals , "true" and "false" are reserved as boolean literals. (#6778)
	isReserved.words = { 'true':1,'false':1,'null':1,'break':1,'else':1,'new':1,'var':1,'case':1,'finally':1,'return':1,'void':1,'catch':1,'for':1,'switch':1,'while':1,'continue':1,'function':1,'this':1,'with':1,'default':1,'if':1,'throw':1,'delete':1,'in':1,'try':1,'do':1,'instanceof':1,'typeof':1,'abstract':1,'enum':1,'int':1,'short':1,'boolean':1,'export':1,'interface':1,'static':1,'byte':1,'extends':1,'long':1,'super':1,'char':1,'final':1,'native':1,'synchronized':1,'class':1,'float':1,'package':1,'throws':1,'const':1,'goto':1,'private':1,'transient':1,'debugger':1,'implements':1,'protected':1,'volatile':1,'double':1,'import':1,'public':1 };

	var getPropParts = function( propNode, parts )
	{
		if ( !parts )
			parts = [];

		var owner		= propNode.getFirstChild(),
			property	= propNode.getLastChild();

		if ( owner )
		{
			if ( owner.getType() == Token.GETPROP )
				getPropParts( owner, parts );
			else
				parts.push( owner );

			parts.push( property );
		}
		else
			parts.push( propNode );

		return parts;
	};

	var writeNode = function( node, opt )
	{
		if ( !node )
			return;

		var type = node.getType();

		if ( type != Token.VAR )
			lastWasVar = false;

		switch ( type )
		{
			case Token.SCRIPT :
				scope = new CKPACKAGER.scope( node, scope );
				scope.noRename = noGlobals;

				tree = node;
				writeChildren( node );
				break;

			case Token.NAME :
			case Token.BINDNAME :
				var name = node.getString();
				if ( constantList[ name ] )
					out( constantList[ name ] );
				else
					out( scope.getNewName( name ) );
				break;

			case Token.VAR :
				if ( lastWasVar )
					out( ',' );
				else
					out( 'var ' );

				var child = node.getFirstChild();
				while ( child )
				{
					var name = child.getString();

					out( scope.getNewName( name, true ) );

					if ( child.getFirstChild() )
					{
						out( '=' );

						if ( child.getFirstChild().getType() == Token.COMMA )
							out( '(' );

						writeChildren( child );

						if ( child.getFirstChild().getType() == Token.COMMA )
							out( ')' );
					}

					scope.declareName( name );

					child = child.getNext();

					if ( child )
						out( ',' );
				}

				lastWasVar = ( node.getNext() && node.getNext().getType() == Token.VAR );

				if ( !lastWasVar && ( !opt || !opt.noSemiColon ) )
					out( ';' );

				break;

			case Token.SETNAME :
				var name = node.getFirstChild().getString();

				writeNode( node.getFirstChild() );

				var value = node.getLastChild();

				switch ( value.getType() )
				{
					case Token.MUL :
					case Token.DIV :
					case Token.MOD :
					case Token.ADD :
					case Token.SUB :
					case Token.BITAND :
					case Token.BITXOR :
					case Token.BITOR :
					case Token.LSH :
					case Token.RSH :
					case Token.URSH :

						if ( ( value.getFirstChild().getType() == Token.NAME && value.getFirstChild().getString() == name ) )
						{
							out( lang[ value.getType() ], '=' );
							writeNode( value.getLastChild() );
							break;
						}

					default :
						out( '=' );

						if ( value.getType() == Token.COMMA )
							out( '(' );

						writeNode( value );

						if ( value.getType() == Token.COMMA )
							out( ')' );

						break;
				}

				break;

			case Token.NEW :
				var child = node.getFirstChild(),
					isArray,
					isObject;

				if ( child.getType() == Token.NAME )
				{
					// Simplify the Array declaration if it has no arguments, or at least two of them.
					// new Array() => []
					// new Array( 5, 10 ) => [5,10]
					// new Array( 5 ) => new Array(5)
					if ( child.getString() == 'Array' && ( !child.getNext() || child.getNext().getNext() ) )
					{
						out( '[' );
						isArray = true;
					}
					else if ( child.getString() == 'Object' && !child.getNext() )
					{
						out( '{' );
						isObject = true;
					}
				}

				if ( !isArray && !isObject )
				{
					out( 'new ' );
					writeNode( child );
					out( '(' );
				}

				child = child.getNext();

				while ( child )
				{
					if ( !writeNode( child ) )
						break;

					child = child.getNext();

					if ( child )
						out( ',' );
				}

				if ( isArray )
					out( ']' );
				else if ( isObject )
					out( '}' );
				else
					out( ')' );
				break;

			case Token.FUNCTION :
				var name = node.getString();

				out( 'function' );

				var currentTree = tree,
					fnIndex = node.getExistingIntProp( Node.FUNCTION_PROP ),
					fn = tree.getFunctionNode(fnIndex);

				if ( fn.functionType != 2 && name.length() )
					out( ' ', scope.getNewName( name, 1 ) );

				scope = new CKPACKAGER.scope( fn, scope );

				// Named function expression affects only inner scope.
				if ( fn.functionType == fn.FUNCTION_EXPRESSION && name.length() )
				{
					scope.declareName( name );
					out( ' ', scope.getNewName( name, 1 ) );
				}

				out( '(' );

				for ( var i = 0, len = scope.args.length ; i < len ; i++ )
				{
					if ( i > 0 )
						out( ',' );
					out( scope.getNewName( scope.args[ i ] ) );
				}

				out( '){' );

				// Initialize the "this" replacement. Changes here must be
				// reflected in the Token.THIS case.
				if ( !scope.functionCount && scope.thisCount > 3 )
					out( 'var ', scope.getNewName( 'this' ), '=this;' );

				tree = fn;

				var funcBody =  fn.getFirstChild();

				// Remove the "scope downcast" provided by Rhino.
				if ( fn.functionType == fn.FUNCTION_EXPRESSION && name.length() )
					funcBody.removeChild( funcBody.getFirstChild() );

				writeNode( funcBody, { isFunction : true } );

				scope = scope.parent;
				tree = currentTree;

				out( '}' );

				// No trailing semicolon for function expression.
				if ( fn.functionType != fn.FUNCTION_EXPRESSION && name.length() )
					out( ';' );
				break;

			case Token.CALL :
				var child = node.getFirstChild(),
					childType = child.getType(),
					isFunctionCall = ( childType == Token.FUNCTION || childType == Token.SETPROP || childType == Token.SETELEM || childType == Token.SETNAME );

				if ( childType == Token.NAME && child.getString() == 'PACKAGER_RENAME' )
				{
					var renamed = child.getNext(),
						renamedRef = '',
						parts = getPropParts( renamed );

					for ( var i = 0 ; i < parts.length ; i++ )
					{
						if ( i > 0 )
							renamedRef += '.';
						renamedRef += parts[ i ].getString();
					}

					// print( '[' + renamedRef + ']' );

					out( 'var ', scope.addRenamedRef( renamedRef ), '=' );
					writeNode( renamed );

					scope.declareName( renamedRef );

					break;
				}

				if ( isFunctionCall )
					out( '(' );

				writeNode( child );

				if ( isFunctionCall )
					out( ')' );

				out( '(' );

				var param = child.getNext();
				while( param )
				{
					if ( param.getType() == Token.COMMA )
						out( '(' );

					if ( !writeNode( param ) )
						break;

					if ( param.getType() == Token.COMMA )
						out( ')' );

					param = param.getNext();

					if ( param )
						out( ',' );
				}

				out( ')' );
				break;

			case Token.LOCAL_BLOCK :
				writeChildren( node );
				break;

			case Token.BLOCK :
				var child = node.getFirstChild(),
					isLoop = opt && opt.isLoop,
					isFunction = opt && opt.isFunction,
					ignoreChildren = false;

				if ( !child )
					break;

				if ( child.getType() == Token.IFNE )
				{
					writeNode( child );
					break;
				}
				else if ( child.getType() == Token.SWITCH )
				{
					out( 'switch(' );
					writeNode( child.getFirstChild() );
					out( '){' );

					var caseChild = child.getFirstChild().getNext();
					while( caseChild )
					{
						writeNode( caseChild );
						caseChild = caseChild.getNext();
					}

					caseChild = child.getNext().target.getNext();
					if ( caseChild )
					{
						out( 'default:' );
						writeChildren( caseChild );
					}

					out( '}' );

					break;
				}
				else if ( child.getType() == Token.ENTERWITH )
				{
					out( 'with(' );
					writeNode( child.getFirstChild() );
					out( '){' );
					writeChildren( child.getNext() );
					out( '}' );
					break;
				}

				while( child )
				{
					if ( child.getType() == Token.CONTINUE )
					{
						if ( !isLoop )
							writeNode( child );
						ignoreChildren = true;
					}
					else if ( child.getType() == Token.RETURN )
					{
						if ( !isFunction || child.getFirstChild() )
							writeNode( child );
						ignoreChildren = true;
					}

					// All children are ignored, except functions.
					if ( !ignoreChildren || child.getType() == Token.FUNCTION )
					{
						if ( !writeNode( child ) )
							break;
					}

					child = child.getNext();
				}

				break;

			case Token.CASE :
				out( 'case' );
				writeNode( node.getFirstChild() );
				out( ':' );
				writeChildren( node.target.getNext() );
				break;

			case Token.BREAK :
				out( 'break;' )
				break;

			case Token.LOOP :
				var child = node.getFirstChild(),
					block,
					isDo = false;

				if ( child.getType() == Token.GOTO )	// while (x) && for (;[x];[x])
				{
					// The block is located two nodes after it.
					block = child.getNext().getNext();

					// The increment expression is two steps from the block, if available.
					var incrementExpression = block.getNext().getNext();

					var expression = child.target.getNext();

					if ( incrementExpression.getType() == Token.EXPR_VOID )	// for
					{
						out( 'for(;' );

						if ( expression.getFirstChild().getType() != Token.TRUE )
							writeNode( expression, { noSemiColon : true } );

						out( ';' );

						writeNode( incrementExpression, { noSemiColon : true } );

						out( ')' );
					}
					else
					{
						if ( expression.getFirstChild().getType() == Token.TRUE )
							out( 'for(;;)' );
						else
						{
							out( 'while(' );
							writeNode( expression );
							out( ')' );
						}
					}
				}
				else if ( child.getType() == Token.TARGET )		// do { ... } while()
				{
					isDo = true;

					out( 'do' );
					block = child.getNext();
				}
				else	// for (x;[x];[x]) / for ( x in o )
				{
					out( 'for(' );

					var hasName = false;

					if ( child.getType() != Token.ENUM_INIT_KEYS )
					{
						writeNode( child, { noSemiColon : true } );

						child = child.getNext();
						hasName = true;
					}

					if ( child.getType() == Token.ENUM_INIT_KEYS )
					{
						block = child.getNext().getNext().getNext();

						// Gets the BINDNAME token.
						if ( !hasName )
							writeNode( block.getFirstChild().getFirstChild().getFirstChild() );

						writeNode( child );

						block = block.getFirstChild().getNext();
					}

					if ( child.getType() == Token.GOTO )
					{
						out( ';' );
						writeNode( child.target.getNext(), { noSemiColon : true } );
						out( ';' );

						block = child.getNext().getNext();

						// The increment expression is two steps from the block, if available.
						var incrementExpression = block.getNext().getNext();

						if ( incrementExpression.getType() == Token.EXPR_VOID )
						{
							writeNode( incrementExpression, { noSemiColon : true } );
						}
					}

					out( ')' );
				}

				var hasCurly = ( !block || !block.getFirstChild() || !!block.getFirstChild().getNext() );

				// Avoid single enclosed 'if...elseif' block combines with any 'else/elseif'
				// statement following the loop. (#4695)
				// E.g. if()for(){if()elseif()}else AND ..elseif()for(){if()}else
				if( !hasCurly )
				{
					try
					{
						block.getFirstChild().getFirstChild().getType() == Token.IFNE && ( hasCurly = 1 );
					}
					catch( er ){}
				}

				if ( hasCurly )
					out( '{' );

				writeNode( block, { isLoop : true } );

				if ( hasCurly )
					out( '}' );

				if ( isDo )
				{
					out( 'while(' );
					writeNode( block.getNext().getNext() );
					out( ')' );
				}

				break;

			case Token.ENUM_INIT_KEYS :
				out( ' in ' );
				writeNode( node.getFirstChild() );
				break;

			case Token.EXPR_RESULT :
			case Token.EXPR_VOID :
				writeChildren( node );

				if ( !opt || !opt.noSemiColon )
					out( ';' );

				break;

			case Token.GETPROP :
				var child = node.getFirstChild(),
					name = child.getNext().getString(),
					finalName = name;

				// Get all parts that compose this node (part1.part2.partN).
				var parts = getPropParts( node ),
					startAt = 0,
					names = [];

				// Get all part names form the start.
				for ( var i = 0 ; i < parts.length ; i++ )
				{
					var part = parts[ i ],
						partType = part.getType();
					if ( partType == Token.NAME || partType == Token.STRING )
						names.push( String( part.getString() ) );
					else
						break;
				}

				// Check if the name parts are to be replaced.
				for ( var i = names.length ; i > 0 ; i-- )
				{
					// Build the full name (e.g. Obj.prop1.prop2).
					var fullName = i == 1 ? names[ 0 ] : names.slice( 0, i ).join( '.' );

					// If we have a property composed by names only.
					if ( i == parts.length && typeof constantList[ fullName ] != 'undefined' )
					{
						out( constantList[ fullName ] )
						return true;
					}

					var newName = scope.getNewName( fullName );

					// If a new names is available.
					if ( newName != fullName )
					{
						// Send the new name.
						out( newName );

						// Removed the replaced names from the parts list.
						startAt = i;
						break;
					}
				}

				for ( var i = startAt ; i < parts.length ; i++ )
				{
					var part = parts[ i ],
						partType = part.getType(),
						parenthesis = !!precedence[ partType ];

					if ( i > 0 )
						out( '.' );

					if ( partType == Token.STRING )
						out( part.getString() );
					else
					{
						if ( parenthesis )
							out( '(' );

						writeNode( part );

						if ( parenthesis )
							out( ')' );
					}
				}

				break;

			case Token.GETELEM :
			case Token.DELPROP :
			case Token.SETPROP :
			case Token.SETELEM :
			case Token.SETPROP_OP :
			case Token.SETELEM_OP :
				var owner = node.getFirstChild(),
					prop = owner.getNext(),
					ownerParenthesis = !!precedence[ owner.getType() ];

				if ( type == Token.DELPROP )
					out( 'delete ' );

				if ( ownerParenthesis )
					out( '(' );

				writeNode( owner );

				if ( ownerParenthesis )
					out( ')' );

				if ( prop.getType() == Token.STRING && !isReserved( prop.getString() ) && /^[a-zA-Z$_][\w$_]*$/.test( prop.getString() ) )
				{
					out( '.' );
					out( prop.getString() );
				}
				else
				{
					out( '[' );
					writeNode( prop );
					out( ']' );
				}

				if ( type == Token.SETPROP || type == Token.SETELEM )
				{
					out( '=' );
					writeNode( node.getLastChild() );
				}

				if ( type == Token.SETPROP_OP || type == Token.SETELEM_OP )
				{
					out( lang[ node.getLastChild().getType() ] );
					out( '=' );
					writeChildren( node.getLastChild() );
				}
				break;

			case Token.IFNE :
				out( 'if(' );
				writeNode( node.getFirstChild() );
				out( ')' );

				var block = node.getNext(),
					hasCurly = true;

				hasCurly =
					(
						!block
						||
						(
							block.getType() == Token.BLOCK
							&&
							(
								!block.getFirstChild()
								||
								block.getFirstChild().getNext()
								||
								(
									(
										block.getFirstChild().getType() == Token.BLOCK
										||
										block.getFirstChild().getType() == Token.IFNE
									)
									&&
									block.getNext().getType() == Token.GOTO
								)
							)
						)
					);

				if ( hasCurly )
					out( '{' );

				writeNode( block );

				if ( hasCurly )
					out( '}' );

				block = block.getNext();
				if ( block.getType() == Token.GOTO )
				{
					out( 'else' );
					block = block.getNext().getNext();

					hasCurly = ( !block || ( block.getType() == Token.BLOCK && ( !block.getFirstChild() || block.getFirstChild().getType() != Token.IFNE && block.getFirstChild().getNext() ) ) );

					if ( hasCurly )
						out( '{' );

					writeNode( block );

					if ( hasCurly )
						out( '}' );
				}

				break;

			case Token.IFEQ :
				writeNode( node.getFirstChild() );
				break;

			case Token.STRING :
				outString( node.getString() );
				break;

			case Token.NUMBER :
				out( node.getDouble() );
				break;

			case Token.TRUE :
				out( 'true' );
				break;

			case Token.FALSE :
				out( 'false' );
				break;

			case Token.ARRAYLIT :
				out( '[' );
				var child = node.getFirstChild();
				while( child )
				{
					if ( !writeNode( child ) )
						break;

					child = child.getNext();

					if ( child )
						out( ',' );
				}
				out( ']' );
				break;

			case Token.OBJECTLIT :
				out( '{' );

				var ids = node.getProp(Node.OBJECT_IDS_PROP),
					counter = 0;

				var child = node.getFirstChild();
				while( child )
				{
					var id = String( ids[ counter++ ] );

					if ( isReserved( id ) || !/^(?:(?:[a-zA-Z$_][\w$_]*)|(?:0|[1-9]\d*))$/.test( id ) )
						outString( id );
					else
						out( id );

					out( ':' );
					if ( !writeNode( child ) )
						break;

					child = child.getNext();

					if ( child )
						out( ',' );
				}

				out( '}' );
				break;

			case Token.THIS :
				// Replace "this" with a shorter name. Changes here must be
				// reflected in the Token.FUNCTION case, which initialized the
				// replacement variable.
				if ( !scope.functionCount && scope.thisCount > 3 )
					out( scope.getNewName( 'this' ) );
				else
					out( 'this' );
				break;

			case Token.NULL :
				out( 'null' );
				break;

			case Token.CONTINUE :
				out ( 'continue', ';' );
				break;

			case Token.RETURN :
				var value = node.getFirstChild();

				out( 'return' );

				if ( value )
					writeNode( value );

				out( ';' );

				break;

			case Token.HOOK :
				node = node.getFirstChild();

				var hasPrecedence =
						associativityRTL[ node.getType() ]
						&& precedence[ node.getType() ]
						&& ( precedence[ Token.HOOK ] <= precedence[ node.getType() ] );

				if ( hasPrecedence )
					out( '(' );

				writeNode( node );

				if ( hasPrecedence )
					out( ')' );

				out( '?' );
				writeNode( node = node.getNext() );
				out( ':' );
				writeNode( node.getNext() );
				break;

			case Token.REGEXP :
				out( '/',
					tree.getRegexpString( node.getExistingIntProp( Node.REGEXP_PROP ) ),
					'/',
					tree.getRegexpFlags( node.getExistingIntProp( Node.REGEXP_PROP ) ) );

				break;

			case Token.TRY :
				// Get the "try" body.
				var child = node.getFirstChild(),
					finallyNode;

				out( 'try{' );
				writeNode( child );
				out( '}' );

				child = child.getNext();

				// If a GOTO node is following it, then we have a "catch" block.
				if ( child.getType() == Token.GOTO )
				{
					// The catch definition is 2 steps later (LOCAL_BLOCK node).
					child = child.getNext().getNext();

					var catchScopeVar = child.getFirstChild().getFirstChild().getString(),
						overrideLocal = !!scope.names[ catchScopeVar ];

					if ( !overrideLocal )
						scope.names[ catchScopeVar ] = scope.declaredNames[ catchScopeVar ] = scope.getNewName( catchScopeVar, true, true );

					out( 'catch(' );
					// Send the catch scope (CATCH_SCOPE).
					writeNode( child.getFirstChild().getFirstChild() );
					out( '){' );

					// Because of the scope, the catch block is in a much deeper node.
					writeNode( child.getFirstChild().getNext().getFirstChild().getNext().getFirstChild() );

					if ( !overrideLocal )
						scope.names[ catchScopeVar ] = scope.declaredNames[ catchScopeVar ] = null;

					out( '}' );
				}

				while ( child )
				{
					if ( child.getType() == Token.FINALLY )
					{
						out( 'finally{' );
						writeNode( child.getFirstChild() );
						out( '}' );
						break;
					}
					child = child.getNext();
				}

				break;

			case Token.THROW :
				out( 'throw ' );
				writeNode( node.getFirstChild() );
				out( ';' );
				break;

			case Token.VOID :
				out( 'void(' );
				writeChildren( node );
				out( ')' );
				break;

			case Token.INSTANCEOF :
				writeNode( node.getFirstChild() );
				out( ' instanceof ' );
				writeNode( node.getLastChild() );
				break;

			case Token.TYPEOFNAME :
				out( 'typeof ', scope.getNewName( node.getString() ) );
				break;

			case Token.TYPEOF :
			case Token.NOT :
			case Token.BITNOT :
			case Token.POS :
			case Token.NEG :
				var child = node.getFirstChild();
				var parenthesis = ( precedence[ type ] < ( precedence[ child.getType() ] || 0 ) );

				out( lang[ type ] );

				if ( parenthesis )
					out( '(' );

				writeNode( child );

				if ( parenthesis )
					out( ')' );

				break;

			case Token.MUL :
			case Token.DIV :
			case Token.MOD :
			case Token.ADD :
			case Token.SUB :
			case Token.RSH :
			case Token.LSH :
			case Token.URSH :
			case Token.LT :
			case Token.LE :
			case Token.GT :
			case Token.GE :
			case Token.EQ :
			case Token.NE :
			case Token.SHEQ :
			case Token.SHNE :
			case Token.BITAND :
			case Token.BITXOR :
			case Token.BITOR :
			case Token.AND :
			case Token.OR :
			case Token.COMMA :
			case Token.IN :
				var left = node.getFirstChild(),
					right = node.getLastChild(),
					parenthesis;

				// Check the operator precedence, to properly isolate things on
				// parenthesis.
				parenthesis = ( precedence[ type ] < ( precedence[ left.getType() ] || 0 ) );

				if ( !parenthesis && associativityRTL[ type ] )
					parenthesis = ( precedence[ type ] == ( precedence[ left.getType() ] || 0 ) );

				if ( parenthesis )
					out( '(' );

				// The left side.
				writeNode( left );

				if ( parenthesis )
					out( ')' );

				// The operator.
				out( lang[ type ] );

				// The right side.
				parenthesis = ( precedence[ type ] < ( precedence[ right.getType() ] || 0 ) );

				if ( !parenthesis && !associativityRTL[ type ] && !noAssociativity[ type ] )
					parenthesis = ( precedence[ type ] == ( precedence[ right.getType() ] || 0 ) );

				if ( parenthesis )
					out( '(' );

				writeNode( right );

				if ( parenthesis )
					out( ')' );

				break;

			case Token.INC :
			case Token.DEC :
				var post = ( node.getExistingIntProp(Node.INCRDECR_PROP) & Node.POST_FLAG ) != 0,
					str = type == Token.INC ? '++' : '--';

				if ( !post )
					out( str );

				out.checkSpace = post;

				writeNode( node.getFirstChild() );

				if ( post )
					out( str );

				out.checkSpace = true;
				break;

			case Token.GOTO:
			case Token.EMPTY:
			case Token.LEAVEWITH:		// TRY
			case Token.USE_STACK:		// SETPROP_OP
				break;

			default :
				print( 'WARNING: Unknown token ' + GetTokenName( type ) );
				break;
		}

		return true;
	};

	var writeChildren = function( node )
	{
		node = node.getFirstChild()

		while ( node )
		{
			if ( !writeNode( node ) )
				break;

			node = node.getNext();
		}
	};

	var regexLib =
	{
		packagerRemove : Pattern.compile( '(?m-s:^.*?@Packager\\.Remove\\.Start).*?(?m-s:@Packager\\.Remove\\.End.*?$)', Pattern.DOTALL ),
		packagerRemoveLine : Pattern.compile( '^.*@Packager\\.RemoveLine.*$', Pattern.MULTILINE ),
		packagerRename : Pattern.compile( '^.*PACKAGER_RENAME\\(\\s*([^\\s]+).*$', Pattern.MULTILINE )
	};

	var preProcess = function( script )
	{
		// Protect IE conditional comments. We are doing it in a way that works
		// for CKEditor, but this is not a generic solution.
		script = script.replace( /\/\*@([\s\S]+?)@\*\/(?:)/g, function( match, contents )
			{
				return 'CKPACKAGER_COND_COMMENT("' + encodeURIComponent( contents ) + '")||'
			});

		// The Java regex here is much faster.
		script = String( regexLib.packagerRemove.matcher( script ).replaceAll( '' ) );
//		script = script.replace( /[^\r\n]*@Packager\.Remove\.Start[\s\S]*?@Packager\.Remove\.End[^\r\n]*/g, '' );

		// The Java regex makes no much difference here instead, but for
		// consistense let's use it again.
		script = String( regexLib.packagerRemoveLine.matcher( script ).replaceAll( '' ) );
//		script = script.replace( /^.*@Packager\.RemoveLine.*/gm, '' );

		script = String( regexLib.packagerRename.matcher( script ).replaceAll( 'PACKAGER_RENAME($1);' ) );

		return script;
	};

	var postProcess = function( script )
	{
		script = script.replace( /CKPACKAGER_COND_COMMENT\(["']([\s\S]+?)["']\)\|\|/g, function( match, contents )
			{
				return '/*@' + decodeURIComponent( contents ) + '@*/'
			});

		return script;
	};

	CKPACKAGER.scriptCompressor =
	{
		compress : function( script, renameGlobals, constants, noCheck, wrap )
		{
			script = preProcess( script );

			// Reset.
			scope = null;
			tree = null;
			lastWasVar = false;
			ignoreSiblings = false;
			isLoop = false;

			if ( wrap )
				script = '(function(){' + script + '})();';

			var compilerEnv = new CompilerEnvirons(),
				errorReporter = compilerEnv.getErrorReporter(),
				parser = new Parser(compilerEnv, errorReporter),
				scriptNode = parser.parse( script, null, 1 );

			noGlobals = !wrap && !renameGlobals;
			constantList = constants || {};

			// Reset the output.
			output = [];
			outputSize = 0;

			// Start writting the "script" node.
			writeNode( scriptNode );

			var compressed = output.join( '' );

			compressed = postProcess( compressed );

			/*
			print( '' );
			print( compressed );
			*/

			// Try to parse the compressed results. If the resulting script is
			// not valid, an error will be thrown here.

			if ( !noCheck )
			{
				print( '' );
				print( '    Checking compressed code...' );
				parser.parse( compressed/*.replace( /;/g, ';\n' )*/, null, 1 );
			}

			return compressed;
		}
	}
})();
