Source: common/ObjectExpression.js

import { Utils, DynamicCachedObject, Expression} from '@uino/base-thing';

const cOperators = ['=', '<', '>', '!'];
const cNameEndCodes = ['|', '&', '||', '&&'];

// #region ObjectWrapper

function ObjectWrapper() {
	this.object = null;
}

Object.assign(ObjectWrapper.prototype, {
	init: function (object) {
		this.object = object;

		return this;
	},
	dispose: function () {
		this.object = null;
	},
});

const _objectWrapper = new ObjectWrapper();
_objectWrapper._name = function (name) {
	return _objectWrapper.object.name == name;
}

_objectWrapper._id = function (id) {
	return _objectWrapper.object.id == id;
}

_objectWrapper._type = function (type) {
	return _objectWrapper.object[type] === true;
}

_objectWrapper._regExp = function (pattern, attribute) {
	let value;
	if (attribute._startsWith('[')) {
		let key = attribute.substr(1, attribute.length - 2)._trimBoth(' \t');
		value = _objectWrapper.object.getAttribute(key);
	}
	else {
		value = _objectWrapper.object.getAttribute(pattern);
	}

	if (Utils.isString(value)) {
		let regExps = ObjectExpression.regExps;

		let regExp = regExps.get(pattern);
		if (!regExp) {
			regExp = new RegExp('^' + attribute._trimRight('*'));

			regExps.set(pattern, regExp);
		}

		return regExp.test(value);
	}

	return false;
}

_objectWrapper._attribute = function (name, pattern) {
	let value = _objectWrapper.object.getAttribute(name);
	if (value) {
		if (pattern) {
			return _objectWrapper._regExp(name, pattern);
		}
	}

	return value;
}

_objectWrapper._hasAttribute = function (name) {
	return _objectWrapper.object.hasAttribute(name);
}
// add by hzy
// get from userData, it will be faster
_objectWrapper._userData = function(name) {
	let value = Utils.getAttribute(_objectWrapper.object.userData, name);
	return value
}

_objectWrapper.tags = function (tags) {
	let expression = ObjectExpression.buildExpression(tags, null, 'tags');
	if (!expression) {
		return false;
	}

	let objectTags = _objectWrapper.object.tags;
	let result = expression.evaluate(objectTags, value => {
		return objectTags.has(value);
	});

	return !!result;
}

_objectWrapper.tags_not = function (tags) {
	let result = _objectWrapper.tags(tags);

	return !result;
}

_objectWrapper.tags_and = function (tags) {
	return _objectWrapper.tags(tags);
}

_objectWrapper.tags_or = function (tags) {
	return _objectWrapper.tags(tags);
}

// #endregion

// #region Private Functions

function _isStringType(value) {
	if (value === 'uuid' || value === 'id' || value === 'name') {
		return true;
	}
	return false
}

function _getTagFuncName(string) {
	if (string._startsWith('tags')) {
		let code = string[4];

		// tags(...)
		if (code == '(') {
			return 'tags';
		}
		// tags:not/and/or
		else if (code == ':') {
			let opString = string.substring(5);
			if (opString._startsWith('not(')) {
				return 'tags_not';
			}
			else if (opString._startsWith('and(')) {
				return 'tags_and';
			}
			else if (opString._startsWith('or(')) {
				return 'tags_or';
			}
		}
	}

	return false;
}

function _isMatchEndCode(string, endCodes) {
	for (let i = 0, l = endCodes.length; i < l; i++) {
		let endCode = endCodes[i];
		if (string.startsWith(endCode)) {
			return true;
		}
	}

	return false;
}

function _getValueString(string, endCodes) {
	let value = '';

	if (Utils.isArray(endCodes)) {
		for (let i = 0, l = string.length; i < l; i++) {
			if (_isMatchEndCode(string.substring(i), endCodes)) {
				break;
			}

			value += string[i];
		}
	}
	else {
		for (let i = 0, l = string.length; i < l; i++) {
			let code = string[i];

			if (endCodes.indexOf(code) !== -1) {
				break;
			}

			value += code;
		}
	}

	return value;
}

function _getOperatorString(string) {
	let opString = '';

	for (let i = 0; i < string.length; i++) {
		let code = string[i];

		if (code != '=' && code != '<' && code != '>' && code != '!') {
			break;
		}

		opString += code;
	}

	// Use '==' to replace '=' or '=='
	if (opString == '=') {
		opString = '==';
	}

	return opString._trimBoth(' \t');
}

function _buildAttributeString(key, string, isStringType, isRegString = false) {
	let expression = key + '(' + "'";

	let attributeName = '';
	let index = 0;
	for (; index < string.length; index++) {
		let code = string[index];

		if (code == '=' || code == '<' || code == '>' || code == '!') {
			break;
		}

		attributeName += code;
	}

	expression += attributeName._trimBoth('" \t');
	expression += "'";
	expression += isRegString ? ',' : ')';

	// Get the value string
	let valueString = string.substring(index);
	if (valueString) {
		// Get the operator string
		let operatorString = _getOperatorString(valueString);
		if (operatorString) {
			// Get the value string (Remove ' ', '\t' and 0...0 of number string)
			valueString = valueString._trimLeft(operatorString)._trimBoth(' \t');

			// If it's not a number string then we convert it as string type
			if (!Utils.isNumberString(valueString) && !Utils.isBooleanString(valueString)) {
				// It's reg-exp
				if (valueString[0] == '/' && valueString[valueString.length - 1] == '/') {
					// Remove '//' both side, we do not need that
					valueString = valueString.substr(1, valueString.length - 2);

					// Remove ')' to concat reg-exp string
					expression = expression._removeAt(expression.length - 1);

					// Do not need operator, here we use to split arguments
					operatorString = ',';

					// Build reg-exp as 2nd arg and pass to _attribute function
					if (valueString[0] != '"' && valueString[0] != "'") {
						valueString = "'" + valueString + "'";
					}

					valueString += ')';
				}
				// It's string
				else {
					if (valueString[0] != '"' && valueString[0] != "'") {
						valueString = "'" + valueString + "'";
					}
				}
			}
			else {
				if (isStringType(attributeName)) {
					// Be sure it has no '"'.
					valueString = valueString._trimBoth('"');
					// Get the value string of 'uuid' and 'id', such as '001', '01'.
					valueString = '"' + valueString + '"';
				}

				// Record string length before it was replaced
				let isEmptyString = true;
				if (valueString.length) {
					isEmptyString = false;
				}

				if (Utils.isNumberString(valueString)) {
					valueString = valueString._replaceAll("^(0+)", "");
				}

				// If now value string is empty and before being replaced value string is not empty
				// then indicates it's 0 number value, so we need to set it to 0 forcely
				if (!valueString && !isEmptyString) {
					valueString = '0';
				}
			}
		}

		if (!isRegString) {
			expression += operatorString;
		}
		expression += valueString;
	}

	if (isRegString) {
		expression += ')';
	}

	return expression;
}

function _buildValueString(key, string, endCodes, isStringType) {
	let expression = '';

	expression += key + '(';
	if (isStringType) {
		expression += '"';
	}

	let value = _getValueString(string, endCodes);
	expression += value._trimBoth('\'"')._trimBoth(' ');

	if (isStringType) {
		expression += '"';
	}
	expression += ')';

	return { expression, value };
}

function _buildEvalExpression(condition, mode) {
	let expression = '';
	for (let i = 0; i < condition.length; i++) {
		let code = condition[i];

		if (code == ' ' || code == '\t') {
			continue;
		}

		// OR
		if (code == '|') {
			if (condition[i + 1] == '|') {
				i++;
			}

			expression += '||';
		}
		// AND
		else if (code == '&') {
			if (condition[i + 1] == '&') {
				i++;
			}

			expression += '&&';
		}
		// NOT and BRACKETS
		else if (code == '!' || code == '(' || code == ')') {
			expression += code;
		}
		// Operations
		else if (code == '+' || code == '-' || code == '*' || code == '/') {
			expression += code;
		}
		// Mode and Attribute
		else if (mode || code == '[') {
			let funcName = '';
			let isRegString = false;

			let info;
			if (mode) {
				let prefix = (mode === 'userData' ? '' : (mode + '='));
				info = _buildValueString('', prefix + condition.substring(i), cNameEndCodes.concat([')']));
				i += info.value.length - prefix.length - 1;
			}
			else {
				info = _buildValueString('', condition.substring(i + 1), ']');
				i += info.value.length + 1; // ']'
			}

			if (mode == "userData") {
				funcName = '_userData';
			}
			else  if (info.value._endsWith('*')) {
				// '[name=car*]'
				funcName = '_regExp';
				isRegString = true;
			}
			else if (info.value._contains(cOperators)) {
				funcName = '_attribute';
			}
			else {
				funcName = '_hasAttribute';
			}

			expression += _buildAttributeString(funcName, info.value, _isStringType, isRegString);
		}
		// ID
		else if (code == '#') {
			let info = _buildValueString('_id', condition.substring(i + 1), ' \t|&', true);
			expression += info.expression;
			i += info.value.length;
		}
		// Type
		else if (code == '.') {
			let info = _buildValueString('_type', 'is' + condition.substring(i + 1), ' \t|&', true);
			expression += info.expression;
			i += info.value.length - 2; // expect 'is' string
		}
		// RegExp
		else if (code == '/') {
			let info = _buildValueString('_regExp', condition.substring(i + 1), '/', true);
			i += info.value.length + 1; // '/'

			let startIndex = condition.substring(i).indexOf('(');
			if (startIndex !== -1) {
				i += startIndex + 1;

				let endIndex = condition.substring(i).indexOf(')');
				if (endIndex !== -1) {
					let argString = condition.substr(i, endIndex);
					expression += `_regExp('${info.value}', '${argString}')`;

					i += endIndex + 1;
				}
			}
		}
		// Name or Tags
		else {
			let info;

			// Check whether it's tags function
			let tagFuncName = _getTagFuncName(condition);
			if (tagFuncName) {
				i += tagFuncName.length + 1;

				info = _buildValueString(tagFuncName, condition.substring(i), [')'], true);

				// Make tags_and(A, B, C) => tag_and(A && B && C)
				if (tagFuncName == 'tags_and') {
					info.expression = info.expression._replaceAll(',', '&&');
				}
				// Make tags_not/tags_or(A, B, C) => tags_not/tags_or(A || B || C)
				else if (tagFuncName == 'tags_not' || tagFuncName == 'tags_or') {
					info.expression = info.expression._replaceAll(',', '||');
				}
			}
			// It's name
			else {
				info = _buildValueString('_name', condition.substring(i), cNameEndCodes, true);
			}

			expression += info.expression;
			i += info.value.length;
		}
	}

	return expression;
}

// #endregion

/**
 * @class ObjectExpression
 * The object expression.
 * @memberof THING
 */
class ObjectExpression {

	static expressions = new DynamicCachedObject({
		liveTime: 10 * 1000,
		cleanupTime: 5 * 1000,
	});

	static tagsExpressions = new DynamicCachedObject({
		liveTime: 10 * 1000,
		cleanupTime: 5 * 1000,
	});

	static regExps = new DynamicCachedObject({
		liveTime: 10 * 1000,
		cleanupTime: 5 * 1000,
	});

	static buildExpression(exp, onMakeExpression, mode) {
		if (Utils.isRegExp(exp)) {
			return exp;
		}
		else {
			let expressions = ObjectExpression.expressions;
			let key = exp;
			// Get tags' expressions
			if (mode === 'tags') {
				expressions = ObjectExpression.tagsExpressions;
				mode = null;
			}
			else if (mode) {
				key = mode + ':' + exp;
			}

			let evalExpression = expressions.get(key);
			if (evalExpression === undefined) {
				let expression = onMakeExpression ? onMakeExpression(exp, mode) : exp;
				try {
					evalExpression = new Expression(expression);
					expressions.set(key, evalExpression);
				}
				catch (ex) {
					expressions.set(key, null);

					Utils.error(ex);
					return null;
				}
			}

			return evalExpression;
		}
	}

	constructor(exp, mode) {
		if (_DEBUG) {
			this._exp = exp;
		}

		this._expression = ObjectExpression.buildExpression(exp, _buildEvalExpression, mode);
		this._isRegExp = Utils.isRegExp(this._expression);
		this._mode = mode;
		return this;
	}

	evaluate(object) {
		let expression = this._expression;
		if (!expression) {
			return null;
		}

		// Test regular expression with object's name
		if (this._isRegExp) {
			return expression.test(object.name);
		}
		// Test expression with object
		else {
			_objectWrapper.init(object);
			let result = expression.evaluate(_objectWrapper);
			_objectWrapper.dispose();

			return result;
		}
	}

	static update(deltaTime) {
		ObjectExpression.expressions.update(deltaTime);
		ObjectExpression.regExps.update(deltaTime);
	}

}

export { ObjectExpression }