/**
 * 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 https://www.egroupware.org
 * @author Andreas Stöckel
 * @copyright EGroupware GmbH 2011-2021
 */

/*egw:uses
	et2_core_common;
*/

import {egw} from "../jsapi/egw_global";

/**
 * 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.
 */
export  function et2_compilePHPExpression(_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 successful 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));
}

const STATE_DEFAULT = 0;
const STATE_ESCAPED = 1;
const STATE_CURLY_BRACE_OPEN = 2;
const STATE_EXPECT_CURLY_BRACE_CLOSE = 3;
const STATE_EXPECT_RECT_BRACE_CLOSE = 4;
const STATE_EXPR_BEGIN = 5;
const 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 : any = 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 "\'":
						result = _php_readString(_p, "'");
						state = STATE_EXPR_END;
						break;
					case "$":
						result = _php_parseVariable(_p);
						state = STATE_EXPR_END;
						break;
					default:
						_p.pos--;
						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) + ";";
}

// 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("", "");
})();*/