/**
 * JavaScript Template Engine
 *
 *	@author Antoni Jakubiak
 *	@copyright @copy; Firma JAKUBIAK, 2006, http://www.jakubiak.biz/
 *	
 *	JavaScript Template Engine allow You to render HTML code
 *	in web browser environment. 
 */

/**
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */


function Template() {
	/**
	 * to by who i am
	 */
	var oThis = this;
	/**
	 * Max depth in recursion parsing
	 */
	this.depth = 50;
	/**
	 * Predefined objects
	 */
	this.objects = new Array(
		/**
		 * parse
		 *	@example	{parse Block} <!-- BEGIN Block -->{get aaa}<!-- END Block -->
		 */
		new TemplateFunction( 'parse', 1, function( value ) { return oThis.parse( value ); } ),
		/**
		 * foreach
		 *	@example	{foreach list Row item} <!-- BEGIN Row --><tr><td>{. item val}</td></tr><!-- END Row -->
		 */
		new TemplateFunction( 'foreach', 3, function( arr, block, item) { 
			var r = ''; var i;
			for ( var i=0; i<arr.length; i++) {
					var tv =  new TemplateVar( item, {key:i, val:arr[i]} );
					oThis.assign( tv );
					r = r + oThis.parse( block );
					oThis.del( tv );
			}
			return r;
		} ),
		/**
		 * range
		 *	create an array with elemenst from range
		 *	@example	{foreach range 1 10 1 block item}
		 */
		new TemplateFunction( 'range', 3, function( first, last, inc ) { 
			var r = new Array();
			for ( var i = first; i <= last; i++ ) {
				r.push( i );
			}
			return r;
		} ),
		/**
		 * . (dot)
		 *	for accessing arrays and objects
		 *	@example	{. array 0}
		 */
		new TemplateFunction( '.', 2, function( obj, key ) { 
			switch ( typeof obj ) {
				case 'object':
					if ( 'undefined' != typeof obj[key] ) {
						return obj[key];
					} 
					return obj;
				default:
				case 'string':
					return obj;
			}
		} ),
		/** 
		 * array
		 *	create an array
		 *	@example	{foreach arracy 3 1 2 3 block item}
		 */
		new TemplateProcedure( 'makeArray', function( stack ) {
			var length = stack.pop();
			var ret = new Array();
			for ( var i = 0; i < length; i++ ) {
				ret.push( stack.pop() );
			}
			stack.push( ret );
		} ),
		/**
		 * default
		 *	if first argument is not empty then return first artument, else return second
		 &	@exmaple	{default a b}
		 */
		new TemplateFunction( 'default', 2, function( a, b ) { return '' != a ? a : b; } ),
		/**
		 * if
		 *	if first artument then return second else third
		 *	@example	{if a b c}
		 */
		new TemplateFunction( 'if', 3, function( a, b, c ) { if ( '' != a ) return b; else return c; } ),
		new TemplateFunction( 'blockif', 3, function( a, b, c ) {if ( '' != a ) return oThis.parse(b); else return oThis.parse(c); } ),
		/**
		 * mathematical
		 *	@example	{+ * 3 2 4}
		 */
		new TemplateFunction( '+', 2, function( a, b ) { return parseFloat( a ) + parseFloat( b ); } ),
		new TemplateFunction( '-', 2, function( a, b ) { return parseFloat( a ) - parseFloat( b ); } ),
		new TemplateFunction( '*', 2, function( a, b ) { return parseFloat( a ) * parseFloat( b ); } ),
		new TemplateFunction( '/', 2, function( a, b ) { return parseFloat( a ) / parseFloat( b ); } ),
		new TemplateFunction( 'round', 1, function( a ) { return Math.round( parseFloat( a ) ); } ),
		new TemplateFunction( 'ceil',  1, function( a ) { return Math.ceil ( parseFloat( a ) ); } ), 
		/**
		 * logical
		 *	@example	{if < a b c d}
		 */
		new TemplateFunction( '==', 2, function( a, b ) { return a == b; } ),
		new TemplateFunction( '!=', 2, function( a, b ) { return a != b; } ),
		new TemplateFunction( '<' , 2, function( a, b ) { return a <  b; } ),
		new TemplateFunction( '>' , 2, function( a, b ) { return a >  b; } ),
		new TemplateFunction( '<=', 2, function( a, b ) { return a <= b; } ),
		new TemplateFunction( '>=', 2, function( a, b ) { return a >= b; } ),

		new TemplateFunction( 'eq', 2, function( a, b ) { return a == b; } ),
		new TemplateFunction( 'ne', 2, function( a, b ) { return a != b; } ),
		new TemplateFunction( 'lt', 2, function( a, b ) { return a <  b; } ),
		new TemplateFunction( 'gt', 2, function( a, b ) { return a >  b; } ),
		new TemplateFunction( 'le', 2, function( a, b ) { return a <= b; } ),
		new TemplateFunction( 'ge', 2, function( a, b ) { return a >= b; } ),
		/**
		 * null
		 */
		new TemplateVar( 'NULL', null ),
		new TemplateVar( 'EMPTY', "" )
	);
}

/**
 * Parse a string
 *	@param string sourceString a string containing a template program
 *	@return string
 *	@example alert( t.parse( "<div>{+ 1 2}</div>" ) );
 *	@access public
 */
Template.prototype.parse = function( sourceString ) { 
	if ( this.depth <= 0 ) {
		return sourceString;
	} 
	this.depth--;
	var templateBlock = new TemplateBlock( 'test', sourceString );
	var returnString = templateBlock.execute( this );
	var re = /{([^}]+)}/g;
	var oThis = this;
	returnString = returnString.replace( re, function( sMatch, commandsString  ) {
		var tmp = oThis.execute( commandsString );
		return tmp.shift();
	} );
	this.depth++;
	return returnString;
}

/**
 * Assign template object to template engine
 *	@param TemplateObject to be assigned to list off all objects
 *	@return null
 *	@example t.assign( new TemplateVar('message','Hello World!') );
 *	@access public
 */
Template.prototype.assign = function( templateObject ) {
	this.objects.push( templateObject );
}

/**
 * Remove template object assigned to template engine
 *	@param TemplateObject to be removed from list off all objects
 *	@return null
 *	@example var x = new TemplateObject('message','Hello World!); t.assign( x ); t.del( x );
 *	@access private
 */
Template.prototype.del = function( templateObject ) {
	var o;
	var no = new Array();
	while( o = this.objects.shift() ) {
		if ( o == templateObject ) continue;
		no.push( o );
	}
	this.objects = no;
}

/**
 * Get a template object assigned to template engine
 *	@param string name of the template object
 *	@return TemplateObject or null assigned to template objects which names match. If, there is more object which matching name it return last assigned.
 *	@example alert( t.get( '+' ) )
 *	@access private
 */
Template.prototype.get = function( name ) {
	var i;
	for ( i = this.objects.length - 1; i >= 0; i-- ) {
		var to = this.objects[ i ];
		if ( to.is( name ) ) {
			return to;
		}
	}
	return null;
}
/**
 * Execute a command string (or array of commands)
 *	comands string it's white space separeted command evalueted using polish notation
 *	@param string commandsString contains white separeted commands
 *	@return array with with evalueted stack
 *	@access private
 */
Template.prototype.execute = function( commandsString ) {
	var commands = null;
	if ( 'array' == typeof commandsString ) { 
		commands = commandsString;
	} else {
		commands = commandsString.split( / +/ );
	}
	var command = null;
	var stack = new Array(); 
	while( command = commands.pop() ) {
		var to = this.get( command );
		if ( ! to ) {
			// it'c label or number
			stack.push( command );
			continue;
		}
		var noa = to.getNumOfArgs();
		if ( '*' == noa  ) {
			// its procedure and operate on stack
			to.execute( this, stack );
		} else {
			// it's command, value or block
			// taking arguments from stack
			var args = new Array();
			var i;
			for ( i = noa - 1; i >= 0; i-- ) {
				args.push( stack.pop() );
			}
			// execute command, get value, or get block
			stack.push( to.execute( this, args ) ); 
		}
	}
	return stack;
}


/**
 * Template objects: functions, values, blocks
 *	Abstract class for Templete vars, blocks, functions, procedures etc.
 */
TemplateObject = function() {}
TemplateObject.prototype.is = function( name ) { return false; }
TemplateObject.prototype.getNumOfArgs = function() { return 0; }
TemplateObject.prototype.execute = function() { return null; }





/**
 * Template function
 */
function TemplateFunction( iName, iNumOfArgs, iCall ) {
	/**
	 * Function name
	 */
	this.name      = iName;
	/**
	 * Num of arguments (taken from stack) for this function
	 */
	this.numOfArgs = iNumOfArgs;
	/**
	 * Callback function
	 *	this.call( templateObject, arg1, arg2, argN );
	 */
	this.call      = iCall;
}
TemplateFunction.prototype.is = function( iName ) { return this.name == iName; }
TemplateFunction.prototype.getNumOfArgs = function() { return this.numOfArgs; }
TemplateFunction.prototype.execute = function( templateObject, args ) { return this.call.apply( templateObject, args ); }



/**
 * Template procedure
 *	operate on stack
 */
function TemplateProcedure( iName, iCall ) {
	/**
	 * Function name
	 */
	this.name      = iName;
	/**
	 * Callback function
	 *	this.call( templateObject, arg1, arg2, argN );
	 */
	this.call      = iCall;
}
TemplateProcedure.prototype.is = function( iName ) { return this.name == iName; }
TemplateProcedure.prototype.getNumOfArgs = function() { return '*'; }
TemplateProcedure.prototype.execute = function( templateObject, args ) { return this.call.call( templateObject, args ); }









/**
 * Template variable
 */
function TemplateVar( iName, iValue ) {
	/**
	 * Variable name
	 */
	this.name  = iName;
	/** 
	 * Variable value (string)
	 */
	this.value = iValue;
}
TemplateVar.prototype.is = function( iName ) { return this.name == iName; }
TemplateVar.prototype.getNumOfArgs = function() { return 0; }
TemplateVar.prototype.execute = function() { return this.value; }

/**
 * Template block
 */
function TemplateBlock( iName, iBody ) {
	/**
	 * Block name
	 */
	this.name = iName;
	/**
	 * Block body (string) 
	 *	may contains other block, for example: <!-- BEGIN foo --> Hello World! <!-- END foo -->
	 *	like in php-lib
	 */
	this.body = iBody;
	/**
	 * If this block was allrady by parsed
	 *	(search for inherited blocks)
	 */
	this.parsed = false;
}
TemplateBlock.prototype.is = function( iName ) { return this.name == iName; }
TemplateBlock.prototype.getNumOfArgs = function() { return 0; }
TemplateBlock.prototype.execute = function( templateObject ) { 
	if ( this.parsed ) {
		return this.body;
	}
	// if it first call chech for inherited block and assign to template
	//	([^\a]*) it's hack for .*
	var re = /<!-- BEGIN ([-_a-zA-Z0-9]+) -->([^\0]*)<!-- END \1 -->/g
	this.body = this.body.replace( re, function( sMatch, blockName, blockBody ) {
		var tb = new TemplateBlock( blockName, blockBody );
		templateObject.assign( tb );
		return '';
	} );
	this.parsed = true;
	return this.body;
}
