/**
 * EGroupware eTemplate2 - A simple PHP expression parser written in JS
 *
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
 * @package etemplate
 * @subpackage api
 * @link http://www.egroupware.org
 * @author Andreas Stöckel
 * @copyright Stylite 2011
 * @version $Id$
 */

/*egw:uses
	et2_core_common;
*/

(function()
{
	"use strict";

	var STATE_DEFAULT = 0;
	var STATE_ESCAPED = 1;
	var STATE_CURLY_BRACE_OPEN = 2;
	var STATE_EXPECT_CURLY_BRACE_CLOSE = 3;
	var STATE_EXPECT_RECT_BRACE_CLOSE = 4;
	var STATE_EXPR_BEGIN = 5;
	var STATE_EXPR_END = 6;

	function _throwParserErr(_p, _err)
	{
		throw("Syntax error while parsing '" + _p.expr + "' at " +
			_p.pos + ", " + _err);
	}

	function _php_parseDoubleQuoteString(_p, _tree)
	{
		// Extract all PHP variables from the string
		var state = STATE_DEFAULT;
		var str = "";

		while (_p.pos < _p.expr.length)
		{
			// Read the current char and then increment the parser position by
			// one
			var c = _p.expr.charAt(_p.pos++);

			switch (state)
			{

				case STATE_DEFAULT:
				case STATE_CURLY_BRACE_OPEN:

					switch (c)
					{
						case '\\':
							state = STATE_ESCAPED;
							break;

						case '$':
							// check for '$$' as used in placeholder syntax, it is NOT expanded and returned as is
							if (_p.expr.charAt(_p.pos) == "$" && state == STATE_DEFAULT)
							{
								_p.pos++;
								str += '$$';
								break;
							}
							// check for '$' as last char, as in PHP "test$" === 'test$', $ as last char is NOT expanded
							if (_p.pos == _p.expr.length)
							{
								str += '$';
								break;
							}
							// check for regular expression "/  $/"
							if (_p.expr.charAt(_p.pos) == '/' && _p.expr.charAt(0) == '/')
							{
								str += '$';
								break;
							}
							if (str)
							{
								_tree.push(str); str = "";
							}

							// Support for the ${[expr] sytax
							if (_p.expr.charAt(_p.pos) == "{" && state != STATE_CURLY_BRACE_OPEN)
							{
								state = STATE_CURLY_BRACE_OPEN;
								_p.pos++;
							}

							if (state == STATE_CURLY_BRACE_OPEN)
							{
								_tree.push(_php_parseVariable(_p));
								state = STATE_EXPECT_CURLY_BRACE_CLOSE;
							}
							else
							{
								_tree.push(_php_parseVariable(_p));
							}

							break;

						case '{':
							state = STATE_CURLY_BRACE_OPEN;
							break;

						default:
							if (state == STATE_CURLY_BRACE_OPEN)
							{
								str += '{';
								state = STATE_DEFAULT;
							}
							str += c;
					}

					break;

				case STATE_ESCAPED:
					str += c;
					break;

				case STATE_EXPECT_CURLY_BRACE_CLOSE:
					// When returning from the variableEx parser,
					// the current char must be a "}"
					if (c != "}")
					{
						_throwParserErr(_p, "expected '}', but got " + c);
					}

					state = STATE_DEFAULT;
					break;
			}
		}

		// Throw an error when reaching the end of the string but expecting
		// "}"
		if (state == STATE_EXPECT_CURLY_BRACE_CLOSE)
		{
			_throwParserErr(_p, "unexpected end of string, expected '}'");
		}

		// Push the last part of the string onto the syntax tree
		if (state == STATE_CURLY_BRACE_OPEN)
		{
			str += "{";
		}

		if (str)
		{
			_tree.push(str);
		}
	}

	// Regular expression which matches on PHP variable identifiers (without the $)
	var PHP_VAR_PREG = /^([A-Za-z0-9_]+)/;

	function _php_parseVariableName(_p)
	{
		// Extract the variable name form the expression
		var vname = PHP_VAR_PREG.exec(_p.expr.substr(_p.pos));

		if (vname)
		{
			// Increment the parser position by the length of vname
			_p.pos += vname[0].length;
			return {"variable": vname[0], "accessExpressions": []};
		}

		_throwParserErr(_p, "expected variable identifier.");
	}

	function _php_parseVariable(_p)
	{
		// Parse the first variable
		var variable = _php_parseVariableName(_p);

		// Parse all following variable access identifiers
		var state = STATE_DEFAULT;

		while (_p.pos < _p.expr.length)
		{
			var c = _p.expr.charAt(_p.pos++);

			switch (state)
			{
				case STATE_DEFAULT:
					switch (c)
					{
						case "[":
							// Parse the expression inside the rect brace
							variable.accessExpressions.push(_php_parseExpression(_p));
							state = STATE_EXPECT_RECT_BRACE_CLOSE;
							break;

						default:
							_p.pos--;
							return variable;
					}
					break;

				case STATE_EXPECT_RECT_BRACE_CLOSE:
					if (c != "]")
					{
						_throwParserErr(_p, " expected ']', but got " + c);
					}

					state = STATE_DEFAULT;
					break;
			}
		}

		return variable;
	}

	/**
	 * Reads a string delimited by the char _delim or the regExp _delim from the
	 * current parser context and returns it.
	 *
	 * @param {object} _p parser contect
	 * @param {string} _delim delimiter
	 * @return {string} string read (or throws an exception)
	 */
	function _php_readString(_p, _delim)
	{
		var state = STATE_DEFAULT;
		var str = "";

		while (_p.pos < _p.expr.length)
		{
			var c = _p.expr.charAt(_p.pos++);

			switch (state)
			{

				case STATE_DEFAULT:
					if (c == "\\")
					{
						state = STATE_ESCAPED;
					}
					else if (c === _delim || (typeof _delim != "string" && _delim.test(c)))
					{
						return str;
					}
					else
					{
						str += c;
					}
					break;

				case STATE_ESCAPED:
					str += c;
					state = STATE_DEFAULT;
					break;
			}
		}

		_throwParserErr(_p, "unexpected end of string while parsing string!");
	}

	function _php_parseExpression(_p)
	{
		var state = STATE_EXPR_BEGIN;
		var result = null;

		while (_p.pos < _p.expr.length)
		{
			var c = _p.expr.charAt(_p.pos++);

			switch (state)
			{
				case STATE_EXPR_BEGIN:
					switch(c)
					{
						// Skip whitespace
						case " ": case "\n": case "\r": case "\t":
							break;

						case "\"":
							result = [];

							var p = _php_parser(_php_readString(_p, "\""));
							_php_parseDoubleQuoteString(p, result);
							state = STATE_EXPR_END;
							break;
						case "\'":
							var result = _php_readString(_p, "'");
							state = STATE_EXPR_END;
							break;
						case "$":
							var result = _php_parseVariable(_p);
							state = STATE_EXPR_END;
							break;
						default:
							_p.pos--;
							var result = _php_readString(_p, /[^A-Za-z0-9_#]/);

							if (!result)
							{
								_throwParserErr(_p, "unexpected char " + c);
							}

							_p.pos--;
							state = STATE_EXPR_END;
							break;
					}
					break;

				case STATE_EXPR_END:
					switch(c)
					{
						// Skip whitespace
						case " ": case "\n": case "\r": case "\t":
							break;

						default:
							_p.pos--;
							return result;
					}

			}
		}

		_throwParserErr(_p, "unexpected end of string while parsing access expressions!");
	}

	function _php_parser(_expr)
	{
		return {
			expr: _expr,
			pos: 0
		};
	}

	function _throwCompilerErr(_err)
	{
		throw("PHP to JS compiler error, " + _err);
	}

	function _php_compileVariable(_vars, _variable)
	{
		if (_vars.indexOf(_variable.variable) >= 0)
		{
			// Attach a "_" to the variable name as PHP variable names may start
			// with numeric values
			var result = "_" + _variable.variable;

			// Create the access functions
			for (var i = 0; i < _variable.accessExpressions.length; i++)
			{
				result += "[" +
					_php_compileString(_vars, _variable.accessExpressions[i]) +
					"]";
			}

			return '(typeof _'+_variable.variable+' != "undefined" && typeof '+result + '!="undefined" && ' + result + ' != null ? ' + result + ':"")';
		}

		_throwCompilerErr("Variable $" + _variable.variable + " is not defined.");
	}

	function _php_compileString(_vars, _string)
	{
		if (!(_string instanceof Array))
		{
			_string = [_string];
		}

		var parts = [];
		var hasString = false;
		for (var i = 0; i < _string.length; i++)
		{
			var part = _string[i];

			if (typeof part == "string")
			{
				hasString = true;
				// Escape all "'" and "\" chars and add the string to the parts array
				parts.push("'" + part.replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "'");
			}
			else
			{
				parts.push(_php_compileVariable(_vars, part));
			}
		}

		if (!hasString) // Force the result to be of the type string
		{
			parts.push('""');
		}

		return parts.join(" + ");
	}

	function _php_compileJSCode(_vars, _tree)
	{
		// Each tree starts with a "string"
		return "return " + _php_compileString(_vars, _tree) + ";";
	}

	/**
	 * Function which compiles the given PHP string to a JS function which can be
	 * easily executed.
	 *
	 * @param _expr is the PHP string expression
	 * @param _vars is an array with variable names (without the PHP $).
	 * 	The parameters have to be passed to the resulting JS function in the same
	 * 	order.
	 */
	this.et2_compilePHPExpression = function(_expr, _vars)
	{
		if (typeof _vars == "undefined")
		{
			_vars = [];
		}

		try {
			// Initialize the parser object and create the syntax tree for the given
			// expression
			var parser = _php_parser(_expr);

			var syntaxTree = [];

			// Parse the given expression as if it was a double quoted string
			_php_parseDoubleQuoteString(parser, syntaxTree);

			// Transform the generated syntaxTree into a JS string
			var js = _php_compileJSCode(_vars, syntaxTree);

			// Log the successfull compiling
			egw.debug("log", "Compiled PHP " + _expr + " --> " + js);
		}
		catch(e) {
			// if expression does NOT compile use it literally and log a warning, but not stop execution
			egw.debug("warn", "Error compiling PHP "+_expr+" --> using it literally ("+
				(typeof e == 'string' ? e : e.message)+")!");
			return function(){ return _expr; };
		}
		// Prepate the attributes for the function constuctor
		var attrs = [];
		for (var i = 0; i < _vars.length; i++)
		{
			attrs.push("_" + _vars[i]);
		}
		attrs.push(js);

		// Create the function and return it
		return (Function.apply(Function, attrs));
	};

}).call(window);

// Include this code in in order to test the above code
/*(function () {
	var row = 10;
	var row_cont = {"title": "Hello World!"};
	var cont = {10: row_cont};

	function test(_php, _res)
	{
		console.log(
			et2_compilePHPExpression(_php, ["row", "row_cont", "cont"])
				(row, row_cont, cont) === _res);
	}

	test("${row}[title]", "10[title]");
	test("{$row_cont[title]}", "Hello World!");
	test('{$cont["$row"][\'title\']}', "Hello World!");
	test("$row_cont[${row}[title]]");
	test("\\\\", "\\");
	test("", "");
})();*/