<?php
/*
 * @copyright   Copyright (C) 2010-2024 Combodo SAS
 * @license     http://opensource.org/licenses/AGPL-3.0
 */

class MissingQueryArgument extends CoreException {
}


class ExpressionHelper {
	/**
	 * Callback to be used with {@link Expression::Browse}, to update the AllowAllData attribute in the NestedQueryExpression that are
	 * present in the Expression tree
	 *
	 * @param \Expression $oExpression
	 * @param boolean $bAllowAllData
	 *
	 * @uses \DBSearch::AllowAllData()
	 *
	 * @since 2.7.2 3.0.0 N°3324
	 */
	public static function ExpressionAllowAllDataCallback($oExpression, $bAllowAllData) {
		if (!($oExpression instanceof NestedQueryExpression)) {
			return;
		}

		$oExpression->AllowAllData($bAllowAllData);
	}
}

class NotYetEvaluatedExpression extends CoreException
{
}

/**
 * @method Check($oModelReflection, array $aAliases, $sSourceQuery)
 */
abstract class Expression {
	const OPERATOR_BINARY = 'binary';
	const OPERATOR_BOOLEAN = 'boolean_binary';
	const OPERATOR_FIELD = 'field';
	const OPERATOR_FUNCTION = 'function';
	const OPERATOR_INTERVAL = 'interval';
	const OPERATOR_LIST = 'list';
	const OPERATOR_SCALAR = 'scalar';
	const OPERATOR_UNARY = 'unary';
	const OPERATOR_VARIABLE = 'variable';


	/**
	 * Perform a deep clone (as opposed to "clone" which does copy a reference to the underlying objects)
	 **/
	public function DeepClone()
	{
		return unserialize(serialize($this));
	}

	// recursive translation of identifiers
	abstract public function GetUnresolvedFields($sAlias, &$aUnresolved);

	/**
	 * @param array $aTranslationData
	 * @param bool $bMatchAll
	 * @param bool $bMarkFieldsAsResolved
	 *
	 * @return Expression Translated expression
	 */
	abstract public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true);

	public final static function ConvertArrayToOQL($aExpressions, $aArgs)
	{
		$aRet = array();
		foreach ($aExpressions as $sName => $oExpression)
		{
			/** @var Expression $oExpression */
			$aRet[$sName] = $oExpression->RenderExpression(false, $aArgs);
		}
		return $aRet;
	}

	public final static function ConvertArrayFromOQL($aExpressions)
	{
		$aRet = array();
		foreach ($aExpressions as $sName => $sConditionExpr)
		{
			/** @var Expression $oExpression */
			$aRet[$sName] = Expression::FromOQL($sConditionExpr);
		}
		return $aRet;
	}


	/**
	 * recursive rendering
	 *
	 * @deprecated 3.0.0 use RenderExpression
	 *
	 * @param array $aArgs used as input by default, or used as output if bRetrofitParams set to True
	 * @param bool $bRetrofitParams
	 *
	 * @return array|string
	 * @throws \MissingQueryArgument
	 */
	public function Render(&$aArgs = null, $bRetrofitParams = false)
	{
		// cannot notify depreciation for now as this is still MASSIVELY used in iTop core !
		DeprecatedCallsLog::NotifyDeprecatedPhpMethod('use RenderExpression');

		return $this->RenderExpression(false, $aArgs, $bRetrofitParams);
	}

	/**
	 * recursive rendering
	 *
	 * @param bool $bForSQL generates code for OQL if false, for SQL otherwise
	 * @param array $aArgs used as input by default, or used as output if bRetrofitParams set to True
	 * @param bool $bRetrofitParams
	 *
	 * @return array|string
	 * @throws \MissingQueryArgument
	 */
	abstract public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false);

	/**
	 * Collect parameters, i.e. :parameter
	 *
	 * @param null $sParentFilter
	 *
	 * @return array
	 */
	public function GetParameters($sParentFilter = null)
	{
		$aParameters = array();
		$unused = $this->RenderExpression(false, $aParameters, true);

		if (!is_null($sParentFilter)) $sParentFilter .= '->';

		$aRet = array();
		foreach($aParameters as $sParameter => $unused)
		{
			if (is_null($sParentFilter))
			{
				$aRet[] = $sParameter;
			}
			else
			{
				if (substr($sParameter, 0, strlen($sParentFilter)) == $sParentFilter)
				{
					$aRet[] = substr($sParameter, strlen($sParentFilter));
				}
			}
		}
		return $aRet;
	}

	/**
	 * Evaluate the value of the expression
	 *
	 * @param array $aArgs
	 *
	 * @throws \Exception if terms cannot be evaluated as scalars
	 */
	abstract public function Evaluate(array $aArgs);

	/**
	 * Recursively renders the expression as a structure (array) suitable for a JSON export
	 *
	 * @param mixed[string] $aArgs
	 * @param boolean $bRetrofitParams
	 * @return mixed[string]
	 */
	abstract public function ToJSON(&$aArgs = null, $bRetrofitParams = false);

	/**
	 * @param DBSearch $oSearch
	 * @param array $aArgs
	 * @param AttributeDefinition $oAttDef
	 *
	 * @param array $aCtx
	 *
	 * @return array parameters for the search form
	 * @throws \MissingQueryArgument
	 */
	public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array())
	{
		return $this->RenderExpression(false, $aArgs);
	}

	public function GetAttDef($aClasses = array())
	{
		return null;
	}

	/**
	 * Recursively browse the expression tree.
	 *
	 * To access variables, specify them using the `use` keyword and the `&` to pass by reference if necessary
	 *
	 * @see https://www.php.net/manual/fr/functions.anonymous.php
	 *
	 * @param Closure $callback with current expression as parameter
	 */
	abstract public function Browse(Closure $callback);

	abstract public function ApplyParameters($aArgs);

	// recursively builds an array of class => fieldname
	abstract public function ListRequiredFields();

	// recursively list field parents ($aTable = array of sParent => dummy)
	abstract public function CollectUsedParents(&$aTable);

	/**
	 * @return boolean true if the expression's value is constant and evaluates to true, false otherwise
	 */
	abstract public function IsTrue();

	// recursively builds an array of [classAlias][fieldName] => value
	abstract public function ListConstantFields();

	// recursively builds an array of parameters to give to current request
	abstract public function ListParameters();

	public function RequiresField($sClass, $sFieldName) {
		// #@# todo - optimize : this is called quite often when building a single query !
		$aRequired = $this->ListRequiredFields();
		if (!in_array($sClass.'.'.$sFieldName, $aRequired)) return false;
		return true;
	}

	/**
	 * @return string
	 * @throws \MissingQueryArgument
	 */
	public function serialize()
	{
		return base64_encode($this->RenderExpression(false));
	}

	/**
	 * @param $sValue
	 *
	 * @return Expression
	 * @throws OQLException
	 */
	static public function unserialize($sValue)
	{
		return self::FromOQL(base64_decode($sValue));
	}

	/**
	 * @param $sConditionExpr
	 *
	 * @return Expression
	 * @throws \OQLException
	 */
	static public function FromOQL($sConditionExpr)
	{
		static $aCache = array();
		if (array_key_exists($sConditionExpr, $aCache))
		{
			return unserialize($aCache[$sConditionExpr]);
		}
		$oOql = new OqlInterpreter($sConditionExpr);
		$oExpression = $oOql->ParseExpression();
		$aCache[$sConditionExpr] = serialize($oExpression);

		return $oExpression;
	}

	static public function FromSQL($sSQL)
	{
		return new SQLExpression($sSQL);
	}

	/**
	 * @param Expression $oExpr
	 *
	 * @return Expression
	 * @throws \CoreException
	 */
	public function LogAnd(Expression $oExpr)
	{
		if ($this->IsTrue()) return clone $oExpr;
		if ($oExpr->IsTrue()) return clone $this;
		return new BinaryExpression($this, 'AND', $oExpr);
	}

	/**
	 * @param Expression $oExpr
	 *
	 * @return Expression
	 * @throws \CoreException
	 */
	public function LogOr(Expression $oExpr)
	{
		return new BinaryExpression($this, 'OR', $oExpr);
	}

	abstract public function RenameParam($sOldName, $sNewName);
	abstract public function RenameAlias($sOldName, $sNewName);

	/**
	 * Make the most relevant label, given the value of the expression
	 *
	 * @param DBSearch oFilter The context in which this expression has been used
	 * @param string sValue The value returned by the query, for this expression
	 * @param string sDefault The default value if no relevant label could be computed
	 *
	 * @return string label
	 */
	public function MakeValueLabel($oFilter, $sValue, $sDefault)
	{
		return $sDefault;
	}

	/**
	 * @param $oSearch
	 * @param array $aArgs
	 * @param bool $bRetrofitParams
	 * @param \AttributeDefinition $oAttDef
	 *
	 * @return array
	 * @throws \MissingQueryArgument
	 */
	public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null)
	{
		return array(
			'widget' => AttributeDefinition::SEARCH_WIDGET_TYPE_RAW,
			'oql' => $this->RenderExpression(false, $aArgs, $bRetrofitParams),
			'label' => $this->Display($oSearch, $aArgs, $oAttDef),
			'source' => get_class($this),
		);
	}

	/**
	 * Split binary expression on given operator
	 *
	 * @param Expression $oExpr
	 * @param string $sOperator
	 * @param array $aAndExpr
	 *
	 * @return array of expressions
	 */
	public static function Split($oExpr, $sOperator = 'AND', &$aAndExpr = array())
	{
		if (($oExpr instanceof BinaryExpression) && ($oExpr->GetOperator() == $sOperator))
		{
			static::Split($oExpr->GetLeftExpr(), $sOperator, $aAndExpr);
			static::Split($oExpr->GetRightExpr(), $sOperator, $aAndExpr);
		}
		else
		{
			$aAndExpr[] = $oExpr;
		}

		return $aAndExpr;
	}
}

class SQLExpression extends Expression
{
	protected $m_sSQL;

	public function __construct($sSQL)
	{
		$this->m_sSQL  = $sSQL;
	}

	public function IsTrue()
	{
		return false;
	}

	// recursive rendering
	public function RenderExpression($bForSql = false, &$aArgs = null, $bRetrofitParams = false)
	{
		return $this->m_sSQL;
	}

	/**
	 * Evaluate the value of the expression
	 * @param array $aArgs
	 * @throws \Exception if terms cannot be evaluated as scalars
*/
	public function Evaluate(array $aArgs)
	{
		throw new Exception('a nested query cannot be evaluated');
	}

	// recursive rendering
	public function toJSON(&$aArgs = null, $bRetrofitParams = false)
	{
		return  null; // TODO should we throw an Exception ??
	}

	public function Browse(Closure $callback)
	{
		$callback($this);
	}

	public function ApplyParameters($aArgs)
	{
	}

	public function GetUnresolvedFields($sAlias, &$aUnresolved)
	{
	}

	public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
	{
		return clone $this;
	}

	public function ListRequiredFields()
	{
		return array();
	}

	public function CollectUsedParents(&$aTable)
	{
	}

	public function ListConstantFields()
	{
		return array();
	}

	public function ListParameters()
	{
		return array();
	}

	public function RenameParam($sOldName, $sNewName)
	{
		// Do nothing, since there is nothing to rename
	}

	public function RenameAlias($sOldName, $sNewName)
	{
		// Do nothing, since there is nothing to rename
	}
}



class BinaryExpression extends Expression
{
	protected $m_oLeftExpr; // filter code or an SQL expression (later?)
	protected $m_oRightExpr;
	protected $m_sOperator;

	/**
	 * @param \Expression $oLeftExpr
	 * @param string $sOperator
	 * @param \Expression $oRightExpr
	 *
	 * @throws \CoreException
	 */
	public function __construct($oLeftExpr, $sOperator, $oRightExpr)
	{
		$this->ValidateConstructorParams($oLeftExpr, $sOperator, $oRightExpr);

		$this->m_oLeftExpr  = $oLeftExpr;
		$this->m_oRightExpr = $oRightExpr;
		$this->m_sOperator  = $sOperator;
	}

	/**
	 * @param $oLeftExpr
	 * @param $sOperator
	 * @param $oRightExpr
	 *
	 * @throws \CoreException if one of the parameter is invalid
	 */
	protected function ValidateConstructorParams($oLeftExpr, $sOperator, $oRightExpr)
	{
		if (!is_object($oLeftExpr))
		{
			throw new CoreException('Expecting an Expression object on the left hand', array('found_type' => gettype($oLeftExpr)));
		}
		if (!is_object($oRightExpr))
		{
			throw new CoreException('Expecting an Expression object on the right hand', array('found_type' => gettype($oRightExpr)));
		}
		if (!$oLeftExpr instanceof Expression)
		{
			throw new CoreException('Expecting an Expression object on the left hand', array('found_class' => get_class($oLeftExpr)));
		}
		if (!$oRightExpr instanceof Expression)
		{
			throw new CoreException('Expecting an Expression object on the right hand', array('found_class' => get_class($oRightExpr)));
		}
		if ((($sOperator == "IN") || ($sOperator == "NOT IN")) && !($oRightExpr instanceof ListExpression || $oRightExpr instanceof NestedQueryExpression))
		{
			throw new CoreException("Expecting a List Expression object on the right hand for operator $sOperator",
				array('found_class' => get_class($oRightExpr)));
		}
	}

	public function IsTrue()
	{
		// return true if we are certain that it will be true
		if ($this->m_sOperator == 'AND')
		{
			if ($this->m_oLeftExpr->IsTrue() && $this->m_oRightExpr->IsTrue()) return true;
		}
		elseif ($this->m_sOperator == 'OR')
		{
			if ($this->m_oLeftExpr->IsTrue() || $this->m_oRightExpr->IsTrue()) return true;
		}
		return false;
	}

	public function GetLeftExpr()
	{
		return $this->m_oLeftExpr;
	}

	public function GetRightExpr()
	{
		return $this->m_oRightExpr;
	}

	public function GetOperator()
	{
		return $this->m_sOperator;
	}

	public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false)
	{
		$sOperator = $this->GetOperator();
		$sLeft = $this->GetLeftExpr()->RenderExpression($bForSQL, $aArgs, $bRetrofitParams);
		$sRight = $this->GetRightExpr()->RenderExpression($bForSQL, $aArgs, $bRetrofitParams);
		return "($sLeft $sOperator $sRight)";
	}

	/**
	 * Evaluate the value of the expression
	 * @param array $aArgs
	 * @return mixed
	 * @throws \Exception if terms cannot be evaluated as scalars
*/
	public function Evaluate(array $aArgs)
	{
		$mLeft = $this->GetLeftExpr()->Evaluate($aArgs);
		$mRight = $this->GetRightExpr()->Evaluate($aArgs);

		$sOperator = $this->GetOperator();
		$sType = null;
		switch($sOperator)
		{
			case '+':
			case '-':
			case '*':
			case '/':
				$sType = 'maths';
				break;
			case '=':
			case '!=':
			case '<>':
				$sType = 'comp';
				break;
			case '>':
			case '>=':
			case '<':
			case '<=':
				$sType = 'numcomp';
				break;
			case 'OR':
			case 'AND':
				$sType = 'logical';
				break;
			case 'LIKE':
				$sType = 'like';
				break;
			case 'NOT LIKE':
				$sType = 'notlike';
				break;
			case 'IN':
				$sType = 'in';
				break;
			case 'NOT IN':
				$sType = 'notin';
				break;
			default:
				throw new Exception("Operator '$sOperator' not yet supported");
		}
		switch ($sType){
			case 'logical':
				$bLeft = static::CastToBool($mLeft);
				$bRight = static::CastToBool($mRight);
				switch ($sOperator)
				{
					case 'OR':
						$result = (int)($bLeft || $bRight);
						break;
					case 'AND':
						$result = (int)($bLeft && $bRight);
						break;
					default:
						throw new Exception("Logic: unknown operator '$sOperator'");
				}
				break;

			case 'maths':
				$iLeft = (int) $mLeft;
				$iRight = (int) $mRight;
				switch ($sOperator)
				{
					case '+' : $result = $iLeft + $iRight; break;
					case '-' : $result = $iLeft - $iRight; break;
					case '*' : $result = $iLeft * $iRight; break;
					case '/' : $result = $iLeft / $iRight; break;
					default:
						throw new Exception("Logic: unknown operator '$sOperator'");
				}
				break;
			case 'comp':
				$left = $mLeft;
				$right = $mRight;
				switch ($sOperator)
				{
					case '=' : $result = ($left == $right); break;
					case '!=' : $result = ($left != $right); break;
					case '<>' : $result = ($left != $right); break;
					default:
						throw new Exception("Logic: unknown operator '$sOperator'");
				}
				break;
			case 'numcomp':
				$iLeft = static::ComparableValue($mLeft);
				$iRight = static::ComparableValue($mRight);
				switch ($sOperator)
				{
					case '=' : $result = ($iLeft == $iRight); break;
					case '>' : $result = ($iLeft > $iRight); break;
					case '<' : $result = ($iLeft < $iRight); break;
					case '>=' : $result = ($iLeft >= $iRight); break;
					case '<=' : $result = ($iLeft <= $iRight); break;
					case '!=' : $result = ($iLeft != $iRight); break;
					case '<>' : $result = ($iLeft != $iRight); break;
					default:
						throw new Exception("Logic: unknown operator '$sOperator'");
				}
				break;
			case 'like':
				$sEscaped = preg_quote($mRight, '/');
				$sEscaped = str_replace(array('%', '_', '\\\\.*', '\\\\.'), array('.*', '.', '%', '_'), $sEscaped);
				$pregRes = preg_match("/$sEscaped/i", $mLeft);
				if ($pregRes === false) {
					throw new Exception("Error in regular expression '$sEscaped'");
				}
				$result = ($pregRes === 1);
				break;
			case 'notlike':
				$sEscaped = preg_quote($mRight, '/');
				$sEscaped = str_replace(array('%', '_', '\\\\.*', '\\\\.'), array('.*', '.', '%', '_'), $sEscaped);
				$pregRes = preg_match("/$sEscaped/i", $mLeft);
				if ($pregRes === false) {
					throw new Exception("Error in regular expression '$sEscaped'");
				}
				$result = ($pregRes !== 1);
				break;
			case 'in':
				$result = in_array($mLeft, $mRight);
				break;
			case 'notin':
				$result = !in_array($mLeft, $mRight);
				break;
		}
		return $result;
	}

	static protected function CastToBool($mValue)
	{
		if (is_string($mValue))
		{
			if (is_numeric($mValue))
			{
				return abs($mValue) > 0;
			}
			return false;
		}
		return (bool)$mValue;
	}
	static protected function ComparableValue($mixed)
	{
		if (is_string($mixed))
		{
			$oDate = new \DateTime($mixed);
			if (($oDate->format('Y-m-d') == $mixed) || ($oDate->format('Y-m-d H:i:s') == $mixed))
			{
				$iRet = $oDate->format('U');
			}
			else
			{
				$iRet = (int) $mixed;
			}
		}
		else
		{
			$iRet = $mixed;
		}

		return $iRet;
	}

	/**
	 * {@inheritDoc}
	 * @throws \MissingQueryArgument
	 * @see Expression::ToJSON()
	 */
	public function ToJSON(&$aArgs = null, $bRetrofitParams = false)
	{
		if (($this->GetOperator() == 'AND') || ($this->GetOperator() == 'OR'))
		{
			return array(
				'type' => static::OPERATOR_BOOLEAN,
				'operator' =>  $this->GetOperator(),
				'left' =>  $this->GetLeftExpr()->ToJSON($aArgs, $bRetrofitParams),
				'right' => $this->GetRightExpr()->ToJSON($aArgs, $bRetrofitParams),
				'oql' => $this->RenderExpression(false, $aArgs, $bRetrofitParams),
			);
		}
		return array(
			'type' => static::OPERATOR_BINARY,
			'operator' =>  $this->GetOperator(),
			'left' =>  $this->GetLeftExpr()->ToJSON($aArgs, $bRetrofitParams),
			'right' => $this->GetRightExpr()->ToJSON($aArgs, $bRetrofitParams),
			'oql' => $this->RenderExpression(false, $aArgs, $bRetrofitParams),
		);
	}

	public function Browse(Closure $callback)
	{
		$callback($this);
		$this->m_oLeftExpr->Browse($callback);
		$this->m_oRightExpr->Browse($callback);
	}

	/**
	 * @param $aArgs
	 *
	 * @throws \CoreException
	 * @throws \MissingQueryArgument
	 */
	public function ApplyParameters($aArgs)
	{
		if ($this->m_oLeftExpr instanceof VariableExpression)
		{
			$this->m_oLeftExpr = $this->m_oLeftExpr->GetAsScalar($aArgs);
		}
		else //if ($this->m_oLeftExpr instanceof Expression)
		{
			$this->m_oLeftExpr->ApplyParameters($aArgs);
		}
		if ($this->m_oRightExpr instanceof VariableExpression)
		{
			$this->m_oRightExpr = $this->m_oRightExpr->GetAsScalar($aArgs);
		}
		else //if ($this->m_oRightExpr instanceof Expression)
		{
			$this->m_oRightExpr->ApplyParameters($aArgs);
		}
	}

	public function GetUnresolvedFields($sAlias, &$aUnresolved)
	{
		$this->GetLeftExpr()->GetUnresolvedFields($sAlias, $aUnresolved);
		$this->GetRightExpr()->GetUnresolvedFields($sAlias, $aUnresolved);
	}

	/**
	 * @param array $aTranslationData
	 * @param bool $bMatchAll
	 * @param bool $bMarkFieldsAsResolved
	 *
	 * @return \BinaryExpression|\Expression
	 * @throws \CoreException
	 */
	public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
	{
		$oLeft = $this->GetLeftExpr()->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
		$oRight = $this->GetRightExpr()->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
		return new BinaryExpression($oLeft, $this->GetOperator(), $oRight);
	}
	
	public function ListRequiredFields()
	{
		$aLeft = $this->GetLeftExpr()->ListRequiredFields();
		$aRight = $this->GetRightExpr()->ListRequiredFields();
		return array_merge($aLeft, $aRight);
	}

	public function CollectUsedParents(&$aTable)
	{
		$this->GetLeftExpr()->CollectUsedParents($aTable);
		$this->GetRightExpr()->CollectUsedParents($aTable);
	}

	public function GetAttDef($aClasses = array())
	{
		$oAttDef = $this->GetLeftExpr()->GetAttDef($aClasses);
		if (!is_null($oAttDef)) return $oAttDef;

		return $this->GetRightExpr()->GetAttDef($aClasses);
	}

	/**
	 * List all constant expression of the form <field> = <scalar> or <field> = :<variable>
	 * Could be extended to support <field> = <function><constant_expression>
	 */
	public function ListConstantFields()
	{
		$aResult = array();
		if ($this->m_sOperator == '=')
		{
			if (($this->m_oLeftExpr instanceof FieldExpression) && ($this->m_oRightExpr instanceof ScalarExpression))
			{
				$aResult[$this->m_oLeftExpr->GetParent()][$this->m_oLeftExpr->GetName()] = $this->m_oRightExpr;
			}
			else if (($this->m_oRightExpr instanceof FieldExpression) && ($this->m_oLeftExpr instanceof ScalarExpression))
			{
				$aResult[$this->m_oRightExpr->GetParent()][$this->m_oRightExpr->GetName()] = $this->m_oLeftExpr;
			}
			else if (($this->m_oLeftExpr instanceof FieldExpression) && ($this->m_oRightExpr instanceof VariableExpression))
			{
				$aResult[$this->m_oLeftExpr->GetParent()][$this->m_oLeftExpr->GetName()] = $this->m_oRightExpr;
			}
			else if (($this->m_oRightExpr instanceof FieldExpression) && ($this->m_oLeftExpr instanceof VariableExpression))
			{
				$aResult[$this->m_oRightExpr->GetParent()][$this->m_oRightExpr->GetName()] = $this->m_oLeftExpr;
			}
		}
		else if ($this->m_sOperator == 'AND')
		{
			// Strictly, this should be done only for the AND operator
			$aResult = array_merge_recursive($this->m_oRightExpr->ListConstantFields(), $this->m_oLeftExpr->ListConstantFields());
		}
		return $aResult;
	}

	public function ListParameters()
	{
		$aLeft = $this->GetLeftExpr()->ListParameters();
		$aRight = $this->GetRightExpr()->ListParameters();
		return array_merge($aLeft, $aRight);
	}

	public function RenameParam($sOldName, $sNewName)
	{
		$this->GetLeftExpr()->RenameParam($sOldName, $sNewName);
		$this->GetRightExpr()->RenameParam($sOldName, $sNewName);
	}

	public function RenameAlias($sOldName, $sNewName)
	{
		$this->GetLeftExpr()->RenameAlias($sOldName, $sNewName);
		$this->GetRightExpr()->RenameAlias($sOldName, $sNewName);
	}

	// recursive rendering

	/**
	 * @param \DBSearch $oSearch
	 * @param null $aArgs
	 * @param null $oAttDef
	 * @param array $aCtx
	 *
	 * @return array|string
	 * @throws \CoreException
	 * @throws \DictExceptionMissingString
	 * @throws \MissingQueryArgument
	 */
	public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array())
	{
		$bReverseOperator = false;
		if (method_exists($oSearch, 'GetJoinedClasses'))
		{
			$aClasses = $oSearch->GetJoinedClasses();
		}
		else
		{
			$aClasses = array($oSearch->GetClass());
		}
		$oLeftExpr = $this->GetLeftExpr();
		if ($oLeftExpr instanceof FieldExpression)
		{
			$oAttDef = $oLeftExpr->GetAttDef($aClasses);
		}
		$oRightExpr = $this->GetRightExpr();
		if ($oRightExpr instanceof FieldExpression)
		{
			$oAttDef = $oRightExpr->GetAttDef($aClasses);
			$bReverseOperator = true;
		}


		if ($bReverseOperator)
		{
			$sRight = $oRightExpr->Display($oSearch, $aArgs, $oAttDef, $aCtx);
			$sLeft = $oLeftExpr->Display($oSearch, $aArgs, $oAttDef, $aCtx);

			// switch left and right expressions so reverse the operator
			// Note that the operation is the same so < becomes > and not >=
			switch ($this->GetOperator())
			{
				case '>':
					$sOperator = '<';
					break;
				case '<':
					$sOperator = '>';
					break;
				case '>=':
					$sOperator = '<=';
					break;
				case '<=':
					$sOperator = '>=';
					break;
				default:
					$sOperator = $this->GetOperator();
					break;
			}
			$sOperator = $this->OperatorToNaturalLanguage($sOperator, $oAttDef);

			return "({$sRight}{$sOperator}{$sLeft})";
		}

		$sLeft = $oLeftExpr->Display($oSearch, $aArgs, $oAttDef, $aCtx);
		$sRight = $oRightExpr->Display($oSearch, $aArgs, $oAttDef, $aCtx);

		$sOperator = $this->GetOperator();
		$sOperator = $this->OperatorToNaturalLanguage($sOperator, $oAttDef);

		return "({$sLeft}{$sOperator}{$sRight})";
	}

	private function OperatorToNaturalLanguage($sOperator, $oAttDef)
	{
		if ($oAttDef instanceof AttributeDateTime)
		{
			return Dict::S('Expression:Operator:Date:'.$sOperator, " $sOperator ");
		}

		return Dict::S('Expression:Operator:'.$sOperator, " $sOperator ");
	}

	/**
	 * @param DBSearch $oSearch
	 * @param null $aArgs
	 * @param bool $bRetrofitParams
	 * @param null $oAttDef
	 *
	 * @return array
	 * @throws \CoreException
	 * @throws \DictExceptionMissingString
	 * @throws \MissingQueryArgument
	 */
	public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null)
	{
		$bReverseOperator = false;
		$oLeftExpr = $this->GetLeftExpr();
		$oRightExpr = $this->GetRightExpr();

		if (method_exists($oSearch, 'GetJoinedClasses'))
		{
			$aClasses = $oSearch->GetJoinedClasses();
		}
		else
		{
			$aClasses = array($oSearch->GetClass());
		}

		$oAttDef = $oLeftExpr->GetAttDef($aClasses);
		if (is_null($oAttDef))
		{
			$oAttDef = $oRightExpr->GetAttDef($aClasses);
			$bReverseOperator = true;
		}

		if (is_null($oAttDef))
		{
			return parent::GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef);
		}


		if ($bReverseOperator)
		{
			$aCriteriaRight = $oRightExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef);
			// $oAttDef can be different now
			$oAttDef = $oRightExpr->GetAttDef($aClasses);
			$aCriteriaLeft = $oLeftExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef);

			// switch left and right expressions so reverse the operator
			// Note that the operation is the same so < becomes > and not >=
			switch ($this->GetOperator())
			{
				case '>':
					$sOperator = '<';
					break;
				case '<':
					$sOperator = '>';
					break;
				case '>=':
					$sOperator = '<=';
					break;
				case '<=':
					$sOperator = '>=';
					break;
				default:
					$sOperator = $this->GetOperator();
					break;
			}
			$aCriteria = self::MergeCriteria($aCriteriaRight, $aCriteriaLeft, $sOperator);
		}
		else
		{
			$aCriteriaLeft = $oLeftExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef);
			// $oAttDef can be different now
			$oAttDef = $oLeftExpr->GetAttDef($aClasses);
			$aCriteriaRight = $oRightExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef);

			$aCriteria = self::MergeCriteria($aCriteriaLeft, $aCriteriaRight, $this->GetOperator());
		}
		$aCriteria['oql'] = $this->RenderExpression(false, $aArgs, $bRetrofitParams);
		$aCriteria['label'] = $this->Display($oSearch, $aArgs, $oAttDef);

		if (isset($aCriteriaLeft['ref']) && isset($aCriteriaRight['ref']) && ($aCriteriaLeft['ref'] != $aCriteriaRight['ref']))
		{
			// Only one Field is supported in the expressions
			$aCriteria['widget'] = AttributeDefinition::SEARCH_WIDGET_TYPE_RAW;
		}

		return $aCriteria;
	}

	protected static function MergeCriteria($aCriteriaLeft, $aCriteriaRight, $sOperator)
	{
		$aCriteriaOverride = array();
		$aCriteriaOverride['operator'] = $sOperator;
		if ($sOperator == 'OR')
		{
			if (isset($aCriteriaLeft['ref']) && isset($aCriteriaRight['ref']) && ($aCriteriaLeft['ref'] == $aCriteriaRight['ref']))
			{
				if (isset($aCriteriaLeft['widget']) && isset($aCriteriaRight['widget']) && ($aCriteriaLeft['widget'] == AttributeDefinition::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY) && ($aCriteriaRight['widget'] == AttributeDefinition::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY))
				{
					$aCriteriaOverride['operator'] = 'IN';
					$aCriteriaOverride['is_hierarchical'] = true;

					if (isset($aCriteriaLeft['values']) && isset($aCriteriaRight['values']))
					{
						$aCriteriaOverride['values'] = array_merge($aCriteriaLeft['values'], $aCriteriaRight['values']);
					}
				}
			}
			if (isset($aCriteriaLeft['widget']) && isset($aCriteriaRight['widget']) && ($aCriteriaLeft['widget'] == AttributeDefinition::SEARCH_WIDGET_TYPE_TAG_SET) && ($aCriteriaRight['widget'] == AttributeDefinition::SEARCH_WIDGET_TYPE_TAG_SET))
			{
				$aCriteriaOverride['operator'] = 'MATCHES';
			}
		}

		return array_merge($aCriteriaLeft, $aCriteriaRight, $aCriteriaOverride);
	}
}


/**
 * @since 2.6.0 N°931 tag fields
 */
class MatchExpression extends BinaryExpression
{
	/** @var \FieldExpression */
	protected $m_oLeftExpr;
	/** @var \ScalarExpression */
	protected $m_oRightExpr;

	/**
	 * MatchExpression constructor.
	 *
	 * @param \FieldExpression $oLeftExpr
	 * @param \Expression $oRightExpr
	 *
	 * @throws \CoreException
	 */
	public function __construct(FieldExpression $oLeftExpr, Expression $oRightExpr)
	{
		if (!$oRightExpr instanceof ScalarExpression && !$oRightExpr instanceof VariableExpression) {
			throw new CoreException('Only instance of ScalarExpression or VariableExpression are allowed in MATCHES '.get_class($oRightExpr).' found');
		}
		parent::__construct($oLeftExpr, 'MATCHES', $oRightExpr);
	}

	public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false)
	{
		$sLeft = $this->GetLeftExpr()->RenderExpression($bForSQL, $aArgs, $bRetrofitParams);
		$sRight = $this->GetRightExpr()->RenderExpression($bForSQL, $aArgs, $bRetrofitParams);

		if ($bForSQL)
		{
			$sRet = "MATCH ($sLeft) AGAINST ($sRight IN BOOLEAN MODE)";
		}
		else
		{
			$sRet = "$sLeft MATCHES $sRight";
		}

		return $sRet;
	}

	/**
	 * Evaluate the value of the expression
	 * @param array $aArgs
	 * @throws \Exception if terms cannot be evaluated as scalars
*/
	public function Evaluate(array $aArgs)
	{
		throw new Exception('evaluation of MATCHES not implemented yet');
	}

	public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
	{
		/** @var \FieldExpression $oLeft */
		$oLeft = $this->GetLeftExpr()->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
		/** @var \ScalarExpression $oRight */
		$oRight = $this->GetRightExpr()->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);

		return new static($oLeft, $oRight);
	}
}


class UnaryExpression extends Expression
{
	protected $m_value;

	public function __construct($value)
	{
		$this->m_value = $value;
	}

	public function IsTrue()
	{
		// return true if we are certain that it will be true
		return ($this->m_value == 1);
	}

	public function GetValue()
	{
		return $this->m_value;
	}

	// recursive rendering
	public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false)
	{
		return CMDBSource::Quote($this->m_value);
	}

	/**
	 * Evaluate the value of the expression
	 * @param array $aArgs
	 * @throws \Exception if terms cannot be evaluated as scalars
*/
	public function Evaluate(array $aArgs)
	{
		return $this->m_value;
	}

	/**
	 * {@inheritDoc}
	 * @throws \MissingQueryArgument
	 * @see Expression::ToJSON()
	 */
	public function ToJSON(&$aArgs = null, $bRetrofitParams = false)
	{
		return array(
			'type' => static::OPERATOR_UNARY,
			'value' => $this->m_value,
			'oql' => $this->RenderExpression(false, $aArgs, $bRetrofitParams),
		);
	}

	public function Browse(Closure $callback)
	{
		$callback($this);
	}

	public function ApplyParameters($aArgs)
	{
	}

	public function GetUnresolvedFields($sAlias, &$aUnresolved)
	{
	}

	public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
	{
		return clone $this;
	}

	public function ListRequiredFields()
	{
		return array();
	}

	public function CollectUsedParents(&$aTable)
	{
	}

	public function ListConstantFields()
	{
		return array();
	}

	public function ListParameters()
	{
		return array();
	}

	public function RenameParam($sOldName, $sNewName)
	{
		// Do nothing
		// really ? what about :param{$iParamIndex} ??
	}

	public function RenameAlias($sOldName, $sNewName)
	{
		// Do nothing
	}
}

class ScalarExpression extends UnaryExpression
{
	/**
	 * ScalarExpression constructor.
	 *
	 * @param $value
	 *
	 * @throws \CoreException
	 */
	public function __construct($value)
	{
		if (!is_scalar($value) && !is_null($value) && (!$value instanceof OqlHexValue))
		{
			throw new CoreException('Attempt to create a scalar expression from a non scalar', array('var_type'=>gettype($value)));
		}
		parent::__construct($value);
	}

	/**
	 * @param array $oSearch
	 * @param array $aArgs
	 * @param AttributeDefinition $oAttDef
	 *
	 * @param array $aCtx
	 *
	 * @return array|string
	 * @throws \Exception
	 */
	public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array())
	{
		if (!is_null($oAttDef))
		{
			if ($oAttDef->IsExternalKey())
			{
				try
				{
					/** @var AttributeExternalKey $oAttDef */
					$sTarget = $oAttDef->GetTargetClass();
					$oObj = MetaModel::GetObject($sTarget, $this->m_value, false);
					if (empty($oObj))
					{
						return Dict::S('Enum:Undefined');
					}

					return $oObj->Get("friendlyname");
				} catch (CoreException $e)
				{
				}
			}

			if (!($oAttDef instanceof AttributeDateTime))
			{
				return $oAttDef->GetAsPlainText($this->m_value);
			}
		}

		if (strpos($this->m_value, '%') === 0)
		{
			return '';
		}

		if (isset($aCtx['date_display']))
		{
			return $aCtx['date_display']->MakeValueLabel($oSearch, $this->m_value, $this->m_value);
		}

		return $this->RenderExpression(false, $aArgs);
	}

	// recursive rendering
	public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false)
	{
		if (is_null($this->m_value))
		{
			$sRet = 'NULL';
		}
		else
		{
			$sRet = CMDBSource::Quote($this->m_value);
		}
		return $sRet;
	}

	/**
	 * Evaluate the value of the expression
	 * @param array $aArgs
	 * @throws \Exception if terms cannot be evaluated as scalars
*/
	public function Evaluate(array $aArgs)
	{
		return $this->m_value;
	}

	/**
	 * {@inheritDoc}
	 * @see Expression::ToJSON()
	 */
	public function ToJSON(&$aArgs = null, $bRetrofitParams = false)
	{
		return array(
			'type' => static::OPERATOR_SCALAR,
			'value' => $this->m_value,
			'oql' => $this->RenderExpression(false, $aArgs, $bRetrofitParams),
		);
	}

	/**
	 * @param $aArgs
	 *
	 * @return \ScalarExpression
	 */
	public function GetAsScalar($aArgs)
	{
		return clone $this;
	}

	/**
	 * @param $oSearch
	 * @param array $aArgs
	 * @param bool $bRetrofitParams
	 * @param \AttributeDefinition $oAttDef
	 *
	 * @return array
	 * @throws \MissingQueryArgument
	 */
	public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null)
	{
		$aCriterion = array();
		switch ((string)($this->m_value))
		{
			case '%Y-%m-%d':
				$aCriterion['unit'] = 'DAY';
				break;
			case '%Y-%m':
				$aCriterion['unit'] = 'MONTH';
				break;
			case '%w':
				$aCriterion['unit'] = 'WEEKDAY';
				break;
			case '%H':
				$aCriterion['unit'] = 'HOUR';
				break;
			default:
				$aValue = array();
				if (!is_null($oAttDef))
				{
					switch (true)
					{
						case ($oAttDef instanceof AttributeExternalField):
							try
							{
								$oFinalAttDef = $oAttDef->GetFinalAttDef();
								if($oFinalAttDef instanceof  AttributeExternalKey)
								{
									if ($this->GetValue() !== 0)
									{
										/** @var AttributeExternalKey $oFinalAttDef */
										$sTarget = $oFinalAttDef->GetTargetClass();
										$oObj = MetaModel::GetObject($sTarget, $this->GetValue());
										$aValue['label'] = $oObj->Get("friendlyname");
										$aValue['value'] = $this->GetValue();
									}
									else
									{
										$aValue['label'] = Dict::S('Enum:Undefined');
										$aValue['value'] = $this->GetValue();
									}
								}
								else
								{
									$aValue['label'] = $this->GetValue();
									$aValue['value'] = $this->GetValue();
								}
								$aCriterion['values'] = array($aValue);
							}
							catch (Exception $e)
							{
								IssueLog::Error($e->getMessage());
							}
							break;
						case ($oAttDef instanceof AttributeTagSet):
							try
							{
								if (!empty($this->GetValue()))
								{
									$aValues = array();
									$oValue = $this->GetValue();
									if (is_string($oValue))
									{
										$oValue = $oAttDef->GetExistingTagsFromString($oValue, true);
									}
									/** @var \ormTagSet $oValue */
									$aTags = $oValue->GetTags();
									foreach($aTags as $oTag)
									{
										$aValue['label'] = $oTag->Get('label');
										$aValue['value'] = $oTag->Get('code');
										$aValues[] = $aValue;
									}
									$aCriterion['values'] = $aValues;
								}
								else
								{
									$aCriterion['has_undefined'] = true;
								}
							} catch (Exception $e)
							{
								IssueLog::Error($e->getMessage());
							}
							break;
						case ($oAttDef instanceof AttributeEnumSet):
							try
							{
								if (!empty($this->GetValue()))
								{
									$aValues = array();
									$sValue = $this->GetValue();
									if (is_string($sValue))
									{
										$aTags = $oAttDef->FromStringToArray($sValue, ' ');
									}
									else
									{
										$aTags = array();
									}
									foreach($aTags as $sLabel => $sValue)
									{
										$aValue['label'] = $sLabel;
										$aValue['value'] = $sValue;
										$aValues[] = $aValue;
									}
									$aCriterion['values'] = $aValues;
								}
								else
								{
									$aCriterion['has_undefined'] = true;
								}
							} catch (Exception $e)
							{
								IssueLog::Error($e->getMessage());
							}
							break;
						case $oAttDef->IsExternalKey():
							try
							{
								if ($this->GetValue() != 0)
								{
									/** @var AttributeExternalKey $oAttDef */
									$sTarget = $oAttDef->GetTargetClass();
									$oObj = MetaModel::GetObject($sTarget, $this->GetValue(), true, true);
									$aValue['label'] = $oObj->Get("friendlyname");
									$aValue['value'] = $this->GetValue();
									$aCriterion['values'] = array($aValue);
								}
								else
								{
									$aValue['label'] = Dict::S('Enum:Undefined');
									$aValue['value'] = $this->GetValue();
									$aCriterion['values'] = array($aValue);
								}
							} catch (Exception $e)
							{
								// This object cannot be seen... ignore
							}
							break;
						default:
							try
							{
								$aValue['label'] = $oAttDef->GetAsPlainText($this->GetValue());
								$aValue['value'] = $this->GetValue();
								$aCriterion['values'] = array($aValue);
							} catch (Exception $e)
							{
								$aValue['label'] = $this->GetValue();
								$aValue['value'] = $this->GetValue();
								$aCriterion['values'] = array($aValue);
							}
							break;
					}
				}
				break;
		}
		$aCriterion['oql'] = $this->RenderExpression(false, $aArgs, $bRetrofitParams);

		return $aCriterion;
	}

}

class TrueExpression extends ScalarExpression
{
	public function __construct()
	{
		parent::__construct(1);
	}

	public function IsTrue()
	{
		return true;
	}
}

class FalseExpression extends ScalarExpression
{
	public function __construct()
	{
		parent::__construct(0);
	}

	public function IsTrue()
	{
		return false;
	}
}

class FieldExpression extends UnaryExpression
{
	protected $m_sParent;
	protected $m_sName;

	public function __construct($sName, $sParent = '')
	{
		parent::__construct("$sParent.$sName");

		$this->m_sParent = $sParent;
		$this->m_sName = $sName;
	}

	public function IsTrue()
	{
		// return true if we are certain that it will be true
		return false;
	}

	public function GetParent() {return $this->m_sParent;}
	public function GetName() {return $this->m_sName;}

	public function SetParent($sParent)
	{
		$this->m_sParent = $sParent;
		$this->m_value = $sParent.'.'.$this->m_sName;
	}

	private function GetClassName($aClasses = array())
	{
		if (isset($aClasses[$this->m_sParent]))
		{
			return $aClasses[$this->m_sParent];
		}
		else
		{
			return $this->m_sParent;
		}
	}

	/**
	 * @param DBObjectSearch $oSearch
	 * @param array $aArgs
	 * @param AttributeDefinition $oAttDef
	 *
	 * @param array $aCtx
	 *
	 * @return array|string
	 * @throws \CoreException
	 * @throws \DictExceptionMissingString
	 * @throws \Exception
	 */
	public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array())
	{
		if (empty($this->m_sParent))
		{
			return "`{$this->m_sName}`";
		}
		if (method_exists($oSearch, 'GetJoinedClasses'))
		{
			$aClasses = $oSearch->GetJoinedClasses();
		}
		else
		{
			$aClasses = array($oSearch->GetClass());
		}
		$sClass = $this->GetClassName($aClasses);
		$sAttName = MetaModel::GetLabel($sClass, $this->m_sName);
		if ($sClass != $oSearch->GetClass())
		{
			$sAttName = MetaModel::GetName($sClass).':'.$sAttName;
		}

		return $sAttName;
	}

	// recursive rendering
	public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false)
	{
		if (empty($this->m_sParent))
		{
			return "`{$this->m_sName}`";
		}
		return "`{$this->m_sParent}`.`{$this->m_sName}`";
	}

	/**
	 * Evaluate the value of the expression
	 * @param array $aArgs
	 * @throws \Exception if terms cannot be evaluated as scalars
*/
	public function Evaluate(array $aArgs)
	{
		$sKey = empty($this->m_sParent) ? $this->m_sName : "{$this->m_sParent}.{$this->m_sName}";
		if (!array_key_exists($sKey, $aArgs))
		{
			throw new Exception("Missing field '$sKey' from context");
		}
		return $aArgs[$sKey];
	}

	/**
	 * {@inheritDoc}
	 * @see Expression::ToJSON()
	 */
	public function ToJSON(&$aArgs = null, $bRetrofitParams = false)
	{
		return array(
			'type' => static::OPERATOR_FIELD,
			'value' => $this->m_value,
			'alias' => $this->m_sParent,
			'field' => $this->m_sName,
			'oql' => $this->RenderExpression(false, $aArgs, $bRetrofitParams),
		);
	}

	/**
	 * @param array $aClasses
	 *
	 * @return \AttributeDefinition|\AttributeInteger|null
	 * @throws \CoreException
	 * @throws \Exception
	 */
	public function GetAttDef($aClasses = array())
	{
		if (!empty($this->m_sParent))
		{
			$sClass = $this->GetClassName($aClasses);
			$aAttDefs = MetaModel::ListAttributeDefs($sClass);
			if (isset($aAttDefs[$this->m_sName]))
			{
				return $aAttDefs[$this->m_sName];
			}
			else
			{
				if ($this->m_sName == 'id')
				{
					$aParams = array(
						'default_value' => 0,
						'is_null_allowed' => false,
						'allowed_values' => null,
						'depends_on' => null,
						'sql' => 'id',
					);

					return new AttributeInteger($this->m_sName, $aParams);
				}
			}
		}

		return null;
	}


	public function ListRequiredFields()
	{
		$sField = empty($this->m_sParent) ? $this->m_sName : "{$this->m_sParent}.{$this->m_sName}";
		return array($sField);
	}

	public function CollectUsedParents(&$aTable)
	{
		$aTable[$this->m_sParent] = true;
	}

	public function GetUnresolvedFields($sAlias, &$aUnresolved)
	{
		if ($this->m_sParent == $sAlias)
		{
			// Add a reference to the field
			$aUnresolved[$this->m_sName] = $this;
		}
		elseif ($sAlias == '')
		{
			// An empty alias means "any alias"
			// In such a case, the results are indexed differently
			$aUnresolved[$this->m_sParent][$this->m_sName] = $this;
		}
	}

	/**
	 * @param array $aTranslationData
	 * @param bool $bMatchAll
	 * @param bool $bMarkFieldsAsResolved
	 *
	 * @return \Expression|\FieldExpression|\FieldExpressionResolved|mixed|\UnaryExpression
	 * @throws \CoreException
	 */
	public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
	{
		if (!array_key_exists($this->m_sParent, $aTranslationData))
		{
			if ($bMatchAll) throw new CoreException('Unknown parent id in translation table', array('parent_id' => $this->m_sParent, 'translation_table' => array_keys($aTranslationData)));

			return clone $this;
		}
		if (!array_key_exists($this->m_sName, $aTranslationData[$this->m_sParent]))
		{
			if (!array_key_exists('*', $aTranslationData[$this->m_sParent]))
			{
				// #@# debug - if ($bMatchAll) MyHelpers::var_dump_html($aTranslationData, true);
				if ($bMatchAll) throw new CoreException('Unknown name in translation table', array('name' => $this->m_sName, 'parent_id' => $this->m_sParent, 'translation_table' => array_keys($aTranslationData[$this->m_sParent])));
				return clone $this;
			}
			$sNewParent = $aTranslationData[$this->m_sParent]['*'];
			$sNewName = $this->m_sName;
			if ($bMarkFieldsAsResolved)
			{
				$oRet = new FieldExpressionResolved($sNewName, $sNewParent);
			}
			else
			{
				$oRet = new FieldExpression($sNewName, $sNewParent);
			}
		}
		else
		{
			$oRet = $aTranslationData[$this->m_sParent][$this->m_sName];
		}
		return $oRet;
	}

	/**
	 * Make the most relevant label, given the value of the expression
	 *
	 * @param DBSearch oFilter The context in which this expression has been used
	 * @param string sValue The value returned by the query, for this expression
	 * @param string sDefault The default value if no relevant label could be computed
	 *
	 * @return string label
	 * @throws \CoreException
	 * @throws \Exception
	 */
	public function MakeValueLabel($oFilter, $sValue, $sDefault)
	{

		$sAttCode = $this->GetName();
		$sParentAlias = $this->GetParent();

		$aSelectedClasses = $oFilter->GetSelectedClasses();
		$sClass = $aSelectedClasses[$sParentAlias];

		$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
		// Set a default value for the general case
		$sRes = null;

		// Exceptions...
		if ($oAttDef->IsExternalKey())
		{
			/** @var AttributeExternalKey $oAttDef */
			$sObjClass = $oAttDef->GetTargetClass();
			$iObjKey = (int)$sValue;
			if ($iObjKey > 0)
			{
				$oObject = MetaModel::GetObjectWithArchive($sObjClass, $iObjKey, true, true);
				$sRes = $oObject->GetHyperlink();
			}
			else
			{
				// Undefined
				$sRes = DBObject::MakeHyperLink($sObjClass, 0);
			}
		}
		elseif ($oAttDef->IsExternalField())
		{
			if (is_null($sValue))
			{
				$sRes = Dict::S('UI:UndefinedObject');
			}
		}
		elseif ($oAttDef instanceof AttributeEnum)
		{
			$sRes = $oAttDef->GetAsPlainText($sValue);
		}
		if(is_null($sRes))
		{
			$sRes = $oAttDef->GetAsHtml($sValue);
		}
		return $sRes;
	}

	public function RenameAlias($sOldName, $sNewName)
	{
		if ($this->m_sParent == $sOldName)
		{
			$this->m_sParent = $sNewName;
		}
	}

	private function GetJoinedFilters($oSearch, $iOperatorCodeTarget)
	{
		$aFilters = array();
		$aPointingToByKey = $oSearch->GetCriteria_PointingTo();
		foreach ($aPointingToByKey as $sExtKey => $aPointingTo)
		{
			foreach($aPointingTo as $iOperatorCode => $aFilter)
			{
				if ($iOperatorCode == $iOperatorCodeTarget)
				{
					foreach($aFilter as $oExtFilter)
					{
						$aFilters[$sExtKey] = $oExtFilter;
					}
				}
			}
		}
		return $aFilters;
	}

	/**
	 * @param DBObjectSearch $oSearch
	 * @param null $aArgs
	 * @param bool $bRetrofitParams
	 * @param AttributeDefinition $oAttDef
	 *
	 * @return array
	 * @throws \CoreException
	 */
	public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null)
	{
		$aCriteria = array();
		$aCriteria['is_hierarchical'] = false;
		// Replace BELOW joins by the corresponding external key for the search
		// Try to detect hierarchical links
		if ($this->m_sName == 'id')
		{
			if (method_exists($oSearch, 'GetCriteria_PointingTo'))
			{
				$aFilters = $this->GetJoinedFilters($oSearch, TREE_OPERATOR_EQUALS);
				if (!empty($aFilters))
				{
					foreach($aFilters as $sExtKey => $oFilter)
					{
						$aSubFilters = $this->GetJoinedFilters($oFilter, TREE_OPERATOR_BELOW);
						foreach($aSubFilters as $oSubFilter)
						{
							/** @var \DBObjectSearch $oSubFilter */
							$sClassAlias = $oSubFilter->GetClassAlias();
							if ($sClassAlias == $this->m_sParent)
							{
								// Hierarchical link detected
								// replace current field with the corresponding external key
								$this->m_sName = $sExtKey;
								$this->m_sParent = $oSearch->GetClassAlias();
								$aCriteria['is_hierarchical'] = true;
							}
						}
					}
				}
			}
		}

		if (method_exists($oSearch, 'GetJoinedClasses'))
		{
			$oAttDef = $this->GetAttDef($oSearch->GetJoinedClasses());
		}
		else
		{
			$oAttDef = $this->GetAttDef($oSearch->GetSelectedClasses());
		}
		if (!is_null($oAttDef))
		{
			$sSearchType = $oAttDef->GetSearchType();
			try
			{
				if ($sSearchType == AttributeDefinition::SEARCH_WIDGET_TYPE_EXTERNAL_KEY)
				{
					if (MetaModel::IsHierarchicalClass($oAttDef->GetTargetClass()))
					{
						$sSearchType = AttributeDefinition::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY;
					}
				}
			}
			catch (CoreException $e)
			{
			}
		}
		else
		{
			$sSearchType = AttributeDefinition::SEARCH_WIDGET_TYPE;
		}

		$aCriteria['widget'] = $sSearchType;
		$aCriteria['ref'] = $this->GetParent().'.'.$this->GetName();
		$aCriteria['class_alias'] = $this->GetParent();

		return $aCriteria;
	}
}

// Has been resolved into an SQL expression
class FieldExpressionResolved extends FieldExpression
{
	protected $m_aAdditionalExpressions;

	public function __construct($mExpression, $sParent = '')
	{
		$this->m_aAdditionalExpressions = array();
		if (is_array($mExpression))
		{
			foreach ($mExpression as $sSuffix => $sExpression)
			{
				if ($sSuffix == '')
				{
					$sName = $sExpression;
				}
				$this->m_aAdditionalExpressions[$sSuffix] = new FieldExpressionResolved($sExpression, $sParent);
			}
		}
		else
		{
			$sName = $mExpression;
		}

		parent::__construct($sName, $sParent);
	}

	/**
	 * @return array of additional expressions for muti-column attributes
	 * @since 2.7.4
	 */
	public function AdditionalExpressions()
	{
		return $this->m_aAdditionalExpressions;
	}

	public function GetUnresolvedFields($sAlias, &$aUnresolved)
	{
	}

	public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
	{
		return clone $this;
	}
}

class VariableExpression extends UnaryExpression
{
	protected $m_sName;

	public function __construct($sName)
	{
		parent::__construct($sName);

		$this->m_sName = $sName;
	}

	public function IsTrue()
	{
		// return true if we are certain that it will be true
		return false;
	}

	public function GetName()
	{
		return $this->m_sName;
	}

	/**
	 * @param \DBSearch $oSearch
	 * @param null $aArgs
	 * @param null $oAttDef
	 * @param array $aCtx
	 *
	 * @return array|mixed|string
	 * @throws \MissingQueryArgument
	 * @throws \Exception
	 */
	public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array())
	{
		$sValue = $this->m_value;
		if (!is_null($aArgs) && (array_key_exists($this->m_sName, $aArgs)))
		{
			$sValue = $aArgs[$this->m_sName];
		}
		elseif (($iPos = strpos($this->m_sName, '->')) !== false)
		{
			$sParamName = substr($this->m_sName, 0, $iPos);
			$oObj = null;
			$sAttCode = 'id';
			if (array_key_exists($sParamName.'->object()', $aArgs))
			{
				$sAttCode = substr($this->m_sName, $iPos + 2);
				$oObj = $aArgs[$sParamName.'->object()'];
			}
			elseif (array_key_exists($sParamName, $aArgs))
			{
				$sAttCode = substr($this->m_sName, $iPos + 2);
				$oObj = $aArgs[$sParamName];
			}
			if (!is_null($oObj))
			{
				if ($sAttCode == 'id')
				{
					$sValue = $oObj->Get("friendlyname");
				}
				else
				{
					$sValue = $oObj->Get($sAttCode);
				}

				return $sValue;
			}
		}
		if (!is_null($oAttDef))
		{
			if ($oAttDef->IsExternalKey())
			{
				try
				{
					/** @var AttributeExternalKey $oAttDef */
					$sTarget = $oAttDef->GetTargetClass();
					$oObj = MetaModel::GetObject($sTarget, $sValue);

					return $oObj->Get("friendlyname");
				}
				catch (CoreException $e)
				{
				}
			}

			return $oAttDef->GetAsPlainText($sValue);
		}

		return $this->RenderExpression(false, $aArgs);
	}

	/**
	 * @param bool $bForSQL
	 * @param array $aArgs
	 * @param bool $bRetrofitParams
	 *
	 * @return array|string
	 * @throws \MissingQueryArgument
	 */
	public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false)
	{
		if (is_null($aArgs))
		{
			return ':'.$this->m_sName;
		}
		elseif (array_key_exists($this->m_sName, $aArgs))
		{
			$res = CMDBSource::Quote($aArgs[$this->m_sName]);
			if (is_array($res))
			{
				$res = implode(', ', $res);
			}
			return $res;
		}
		elseif (($iPos = strpos($this->m_sName, '->')) !== false)
		{
			$sParamName = substr($this->m_sName, 0, $iPos);
			if (array_key_exists($sParamName.'->object()', $aArgs))
			{
				$sAttCode = substr($this->m_sName, $iPos + 2);
				$oObj = $aArgs[$sParamName.'->object()'];
				if ($sAttCode == 'id')
				{
					return CMDBSource::Quote($oObj->GetKey());
				}
				return CMDBSource::Quote($oObj->Get($sAttCode));
			}
		}

		if ($bRetrofitParams)
		{
			$aArgs[$this->m_sName] = null;
			return ':'.$this->m_sName;
		}
		else
		{
			throw new MissingQueryArgument('Missing query argument', array('expecting'=>$this->m_sName, 'available'=>array_keys($aArgs)));
		}
	}

	/**
	 * Evaluate the value of the expression
	 * @param array $aArgs
	 * @throws \Exception if terms cannot be evaluated as scalars
*/
	public function Evaluate(array $aArgs)
	{
		throw new Exception('not implemented yet');
	}

	/**
	 * {@inheritDoc}
	 * @see Expression::ToJSON()
	 */
	public function ToJSON(&$aArgs = null, $bRetrofitParams = false)
	{
		return array(
			'type' => static::OPERATOR_VARIABLE,
			'value' => $this->m_value,
			'oql' => $this->RenderExpression(false, $aArgs, $bRetrofitParams),
		);
	}

	public function RenameParam($sOldName, $sNewName)
	{
		if ($this->m_sName == $sOldName)
		{
			$this->m_sName = $sNewName;
		}
	}

	/**
	 * @param $aArgs
	 *
	 * @return \ListExpression|\ScalarExpression|null
	 * @throws \CoreException
	 * @throws \MissingQueryArgument
	 * @throws \Exception
	 */
	public function GetAsScalar($aArgs)
	{
		$oRet = null;
		if (array_key_exists($this->m_sName, $aArgs))
		{
			if(is_array($aArgs[$this->m_sName]))
			{
				$aExpressions = array();
				foreach($aArgs[$this->m_sName] as $sValue)
				{
					$aExpressions[] = new ScalarExpression($sValue);
				}
				$oRet = new ListExpression($aExpressions);
			}
			else
			{
				$oRet = new ScalarExpression($aArgs[$this->m_sName]);
			}
		}
		elseif (($iPos = strpos($this->m_sName, '->')) !== false)
		{
			$sParamName = substr($this->m_sName, 0, $iPos);
			if (array_key_exists($sParamName.'->object()', $aArgs))
			{
				$sAttCode = substr($this->m_sName, $iPos + 2);
				$oObj = $aArgs[$sParamName.'->object()'];
				if ($sAttCode == 'id')
				{
					$oRet = new ScalarExpression($oObj->GetKey());
				}
				elseif (MetaModel::IsValidAttCode(get_class($oObj), $sAttCode))
				{
					$oRet = new ScalarExpression($oObj->Get($sAttCode));
				}
				else
				{
					throw new CoreException("Query argument {$this->m_sName} not matching any attribute of class ".get_class($oObj));
				}
			}
		}
		if (is_null($oRet))
		{
			throw new MissingQueryArgument('Missing query argument', array('expecting'=>$this->m_sName, 'available'=>array_keys($aArgs)));
		}
		return $oRet;
	}

	public function ListParameters()
	{
		return array($this);
	}

}

// Temporary, until we implement functions and expression casting!
// ... or until we implement a real full text search based in the MATCH() expression
class ListExpression extends Expression
{
	protected $m_aExpressions;

	public function __construct($aExpressions)
	{
		$this->m_aExpressions = $aExpressions;
	}

	/**
	 * @param $aScalars
	 *
	 * @return \ListExpression
	 * @throws \CoreException
	 */
	public static function FromScalars($aScalars)
	{
		$aExpressions = array();
		foreach($aScalars as $value)
		{
			$aExpressions[] = new ScalarExpression($value);
		}
		return new ListExpression($aExpressions);
	}

	public function IsTrue()
	{
		// return true if we are certain that it will be true
		return false;
	}

	public function GetItems()
	{
		return $this->m_aExpressions;
	}

	// recursive rendering

	/**
	 * @param bool $bForSQL
	 * @param null $aArgs
	 * @param bool $bRetrofitParams
	 *
	 * @return array|string
	 */
	public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false)
	{
		$aRes = array();
		foreach ($this->m_aExpressions as $oExpr)
		{
			$aRes[] = $oExpr->RenderExpression($bForSQL, $aArgs, $bRetrofitParams);
		}
		return '('.implode(', ', $aRes).')';
	}

	/**
	 * Evaluate the value of the expression
	 * @param array $aArgs
	 * @throws \Exception if terms cannot be evaluated as scalars
*/
	public function Evaluate(array $aArgs)
	{
		//throw new Exception('list expression not yet supported');
		$aResult = [];
		foreach ($this->m_aExpressions as $oExpressions) {
			$aResult[] = $oExpressions->Evaluate($aArgs);
		}
		return $aResult;
	}

	/**
	 * {@inheritDoc}
	 * @see Expression::ToJSON()
	 */
	public function ToJSON(&$aArgs = null, $bRetrofitParams = false)
	{
		$aFields = array();
		foreach ($this->m_aExpressions as $oExpr)
		{
			$aFields[] = $oExpr->ToJSON($aArgs, $bRetrofitParams);
		}
		return array(
			'type' => static::OPERATOR_LIST,
			'fields' => $aFields,
			'oql' => $this->RenderExpression(false, $aArgs, $bRetrofitParams),
		);
	}

	public function Browse(Closure $callback)
	{
		$callback($this);
		foreach ($this->m_aExpressions as $oExpr)
		{
			$oExpr->Browse($callback);
		}
	}

	/**
	 * @param $aArgs
	 *
	 * @throws \CoreException
	 * @throws \MissingQueryArgument
	 */
	public function ApplyParameters($aArgs)
	{
		foreach ($this->m_aExpressions as $idx => $oExpr)
		{
			if ($oExpr instanceof VariableExpression)
			{
				$oVarExpr = $oExpr->GetAsScalar($aArgs);
				if ($oVarExpr instanceof ListExpression)
				{
					$this->m_aExpressions = $oVarExpr->GetItems();
				}
				else
				{
					$this->m_aExpressions[$idx] = $oVarExpr;
				}
			}
			else
			{
				$oExpr->ApplyParameters($aArgs);
			}
		}
	}

	public function GetUnresolvedFields($sAlias, &$aUnresolved)
	{
		foreach ($this->m_aExpressions as $oExpr)
		{
			$oExpr->GetUnresolvedFields($sAlias, $aUnresolved);
		}
	}

	public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
	{
		$aRes = array();
		foreach ($this->m_aExpressions as $oExpr)
		{
			$aRes[] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
		}
		return new ListExpression($aRes);
	}

	public function ListRequiredFields()
	{
		$aRes = array();
		foreach ($this->m_aExpressions as $oExpr)
		{
			$aRes = array_merge($aRes, $oExpr->ListRequiredFields());
		}
		return $aRes;
	}

	public function CollectUsedParents(&$aTable)
	{
		foreach ($this->m_aExpressions as $oExpr)
		{
			$oExpr->CollectUsedParents($aTable);
		}
	}

	public function ListConstantFields()
	{
		$aRes = array();
		foreach ($this->m_aExpressions as $oExpr)
		{
			$aRes = array_merge($aRes, $oExpr->ListConstantFields());
		}
		return $aRes;
	}

	public function ListParameters()
	{
		$aRes = array();
		foreach ($this->m_aExpressions as $oExpr)
		{
			$aRes = array_merge($aRes, $oExpr->ListParameters());
		}
		return $aRes;
	}

	public function RenameParam($sOldName, $sNewName)
	{
		foreach ($this->m_aExpressions as $key => $oExpr)
		{
			$this->m_aExpressions[$key] = $oExpr->RenameParam($sOldName, $sNewName);
		}
	}

	public function RenameAlias($sOldName, $sNewName)
	{
		foreach ($this->m_aExpressions as $key => $oExpr)
		{
			$oExpr->RenameAlias($sOldName, $sNewName);
		}
	}

	public function GetAttDef($aClasses = array())
	{
		foreach($this->m_aExpressions as $oExpression)
		{
			$oAttDef = $oExpression->GetAttDef($aClasses);
			if (!is_null($oAttDef)) return $oAttDef;
		}

		return null;
	}

	public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null)
	{
		$aValues = array();

		foreach($this->m_aExpressions as $oExpression)
		{
			$aCrit = $oExpression->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef);
			if (array_key_exists('values', $aCrit))
			{
				$aValues = array_merge($aValues, $aCrit['values']);
			}
		}

		return array('values' => $aValues);
	}
}

class NestedQueryExpression extends Expression
{
	/** @var DBSearch */
	protected $m_oNestedQuery;

	/*$m_oNestedQuery is an DBSearch object*/
	public function __construct($oNestedQuery)
	{
		$this->m_oNestedQuery = $oNestedQuery;
	}

	/**
	 * @param OQLObjectQuery $oObjQuery
	 *
	 * @return \NestedQueryExpression
	 */
	public static function FromOQLObjectQuery($oObjQuery)
	{
		$oExpressions = $oObjQuery->ToDBSearch("");
		return new NestedQueryExpression($oExpressions);
	}

	public function IsTrue()
	{
		// return true if we are certain that it will be true
		return false;
	}
	public function GetNestedQuery()
	{
		return $this->m_oNestedQuery;
	}

	// recursive rendering
	/**
	 * @param bool $bForSQL
	 * @param null $aArgs
	 * @param bool $bRetrofitParams
	 *
	 * @return array|string
	 * @throws \CoreException
	 * @throws \MissingQueryArgument
	 */
	public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false)
	{
		if ($bForSQL)
		{
			$aAttToLoad = array();
			foreach ($this->m_oNestedQuery->GetSelectedClasses() as $sClassAlias => $sClass)
			{
				$aAttToLoad[$sClassAlias] = array();
			}
			return  '('.$this->m_oNestedQuery->MakeSelectQuery(array(), $aArgs, $aAttToLoad).')';
		}
		else
		{
			return '('.$this->m_oNestedQuery->ToOQL(false, null, false).')';
		}
	}

	/**
	 * Evaluate the value of the expression
	 * @param array $aArgs
	 * @throws \Exception if terms cannot be evaluated as scalars
*/
	public function Evaluate(array $aArgs)
	{
		throw new Exception('a nested query cannot be evaluated');
	}

	public function Browse(Closure $callback)
	{
		$callback($this);
	}

	/**/
	public function ApplyParameters($aArgs) {
		$this->m_oNestedQuery->ApplyParameters($aArgs);
	}

	/**/
	public function GetUnresolvedFields($sAlias, &$aUnresolved) {
	}

	/**/
	public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) {
		// Check and prepare the select information
		$this->m_oNestedQuery->TranslateConditions($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);

		return clone $this;
	}

	public function ListRequiredFields() {
		return array();
	}

	public function CollectUsedParents(&$aTable) {
	}

	public function ListConstantFields() {
		return $this->m_oNestedQuery->ListConstantFields();
	}

	public function ListParameters() {
		return $this->m_oNestedQuery->GetExpectedArguments();
	}

	public function RenameParam($sOldName, $sNewName) {
		$this->m_oNestedQuery->RenameParam($sOldName, $sNewName);
	}

	public function RenameAlias($sOldName, $sNewName) {
		$this->m_oNestedQuery->RenameAlias($sOldName, $sNewName);
	}

	/**
	 * @inheritDoc
	 */
	public function ToJSON(&$aArgs = null, $bRetrofitParams = false) {
		return $this->m_oNestedQuery->ToJSON();
	}

	/**
	 * Simple indirection to {@link \DBObjectSearch::AllowAllData()}
	 *
	 * @param bool $bAllowAllData
	 *
	 * @uses \DBSearch::AllowAllData()
	 */
	public function AllowAllData($bAllowAllData = true) {
		$this->m_oNestedQuery->AllowAllData($bAllowAllData);
	}
}

class FunctionExpression extends Expression
{
	protected $m_sVerb;
	protected $m_aArgs; // array of expressions

	public function __construct($sVerb, $aArgExpressions)
	{
		$this->m_sVerb = $sVerb;
		$this->m_aArgs = $aArgExpressions;
	}

	public function IsTrue()
	{
		// return true if we are certain that it will be true
		return false;
	}

	public function GetVerb()
	{
		return $this->m_sVerb;
	}

	public function GetArgs()
	{
		return $this->m_aArgs;
	}

	// recursive rendering
	public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false)
	{
		$aRes = array();
		foreach ($this->m_aArgs as $iPos => $oExpr)
		{
			$aRes[] = $oExpr->RenderExpression($bForSQL, $aArgs, $bRetrofitParams);
		}
		return $this->m_sVerb.'('.implode(', ', $aRes).')';
	}

	/**
	 * Evaluate the value of the expression
	 * @param array $aArgs
	 * @throws \Exception if terms cannot be evaluated as scalars
*/
	public function Evaluate(array $aArgs)
	{
		switch($this->m_sVerb)
		{
			case 'CONCAT':
				$sRet = '';
				foreach ($this->m_aArgs as $iPos => $oExpr)
				{
					$item = $oExpr->Evaluate($aArgs);
					if (is_null($item)) return null;
					$sRet .= $item;
				}
				return $sRet;

			case 'CONCAT_WS':
				if (count($this->m_aArgs) < 3)
				{
					throw new \Exception("Function {$this->m_sVerb} requires at least 3 arguments");
				}
				$sSeparator = $this->m_aArgs[0]->Evaluate($aArgs);
				foreach ($this->m_aArgs as $iPos => $oExpr)
				{
					if ($iPos == 0) continue;
					$item = $oExpr->Evaluate($aArgs);
					if (is_null($item)) return null;
					$aStrings[] = $item;
				}
				$sRet = implode($sSeparator, $aStrings);
				return $sRet;

			case 'SUBSTR':
				if (count($this->m_aArgs) < 2)
				{
					 throw new \Exception("Function {$this->m_sVerb} requires at least 2 arguments");
				}
				$sString = $this->m_aArgs[0]->Evaluate($aArgs);
				$iRawPos = $this->m_aArgs[1]->Evaluate($aArgs);
				$iPos = $iRawPos > 0 ?
					$iRawPos - 1// 0-based in PHP (1-based in SQL)
					: $iRawPos; // Negative
				if (count($this->m_aArgs) == 2)
				{
					// Up to the end of the string
					$sRet = substr($sString, $iPos);
				}
				else
				{
					// Length specified
					$iLen = $this->m_aArgs[2]->Evaluate($aArgs);
					$sRet = substr($sString, $iPos, $iLen);
				}
				return $sRet;

			case 'TRIM':
				if (count($this->m_aArgs) != 1)
				{
					throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
				}
				$sRet = trim($this->m_aArgs[0]->Evaluate($aArgs));
				return $sRet;

			case 'INET_ATON':
				if (count($this->m_aArgs) != 1)
				{
					throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
				}
				$sRet = ip2long($this->m_aArgs[0]->Evaluate($aArgs));
				return $sRet;

			case 'INET_NTOA':
				if (count($this->m_aArgs) != 1)
				{
					throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
				}
				$sRet = long2ip($this->m_aArgs[0]->Evaluate($aArgs));
				return $sRet;

			case 'ISNULL':
				if (count($this->m_aArgs) != 1)
				{
					throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
				}
				$sRet = is_null($this->m_aArgs[0]->Evaluate($aArgs));
				return $sRet;

			case 'COALESCE':
				if (count($this->m_aArgs) < 1)
				{
					throw new \Exception("Function {$this->m_sVerb} requires at least 1 argument");
				}
				$ret = null;
				foreach($this->m_aArgs as $iPos => $oExpr)
				{
					$ret = $oExpr->Evaluate($aArgs);
					if (!is_null($ret)) break;
				}
				return $ret;

			case 'IF':
				if (count($this->m_aArgs) != 3)
				{
					throw new \Exception("Function {$this->m_sVerb} requires 3 arguments");
				}
				$bCond = $this->m_aArgs[0]->Evaluate($aArgs);
				if ($bCond)
				{
					$ret = $this->m_aArgs[1]->Evaluate($aArgs);
				}
				else
				{
					$ret = $this->m_aArgs[2]->Evaluate($aArgs);
				}
				return $ret;

			case 'ELT':
				if (count($this->m_aArgs) < 2)
				{
					throw new \Exception("Function {$this->m_sVerb} requires at least 2 arguments");
				}
				// First argument is the 1-based position
				$iPosition = (int) $this->m_aArgs[0]->Evaluate($aArgs);
				if (($iPosition == 0) || ($iPosition >= count($this->m_aArgs)))
				{
					// Out of range
					$ret = null;
				}
				else
				{
					$ret = $this->m_aArgs[$iPosition]->Evaluate($aArgs);
				}
				return $ret;

			case 'DATE':
				if (count($this->m_aArgs) != 1)
				{
					throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
				}
				$sRet = date('Y-m-d', strtotime($this->m_aArgs[0]->Evaluate($aArgs)));
				return $sRet;

			case 'YEAR':
				if (count($this->m_aArgs) != 1)
				{
					throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
				}
				$iRet = (int) date('Y', strtotime($this->m_aArgs[0]->Evaluate($aArgs)));
				return $iRet;

			case 'MONTH':
				if (count($this->m_aArgs) != 1)
				{
					throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
				}
				$iRet = (int) date('m', strtotime($this->m_aArgs[0]->Evaluate($aArgs)));
				return $iRet;

			case 'DAY':
				if (count($this->m_aArgs) != 1)
				{
					throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
				}
				$iRet = (int) date('d', strtotime($this->m_aArgs[0]->Evaluate($aArgs)));
				return $iRet;

			case 'DATE_FORMAT':
				if (count($this->m_aArgs) != 2) {
					throw new \Exception("Function {$this->m_sVerb} requires 2 arguments");
				}
				$oDate = new DateTime($this->m_aArgs[0]->Evaluate($aArgs));
				$sFormatForMysqlDateFormat = $this->m_aArgs[1]->Evaluate($aArgs);

				if (preg_match('/%[fUuVX]/', $sFormatForMysqlDateFormat)) {
					throw new NotYetEvaluatedExpression("Expression ".$this->RenderExpression().' cannot be evaluated (known limitation)');
				}

				if (preg_match('/%j/', $sFormatForMysqlDateFormat)) {
					$sFormatForMysqlDateFormat = str_replace('%j', 'z', $sFormatForMysqlDateFormat);
					$sRet = date_format($oDate, $sFormatForMysqlDateFormat);
					$sRet++;
					/** @noinspection PhpUnnecessaryLocalVariableInspection */
					$sRet = str_pad($sRet, 3, '0', STR_PAD_LEFT);

					return $sRet;
				}

				/**
				 * @var string[] $aFormatsForMysqlDateFormat
				 * @link https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_date-format
				 */
				//@formatter:off we want to keep every single item on its own line to ease comp between MySQL and PHP formats !
				$aFormatsForMysqlDateFormat = [
					'%y',
					'%x',
					'%w',
					'%W',
					'%v',
					'%T',
					'%S',
					'%r',
					'%p',
					'%M',
					'%l',
					'%k',
					'%I',
					'%h',
					'%b',
					'%a',
					'%D',
					'%c',
					'%e',
					'%Y',
					'%d',
					'%m',
					'%H',
					'%i',
					'%s'
				];
				//@formatter:on
				/**
				 * @var string[] $aFormatsForPhpDateFormat
				 * @link https://www.php.net/manual/en/datetime.format.php
				 */
				//@formatter:off we want to keep every single item on its own line to ease comp between MySQL and PHP formats !
				$aFormatsForPhpDateFormat = [
					'y',
					'o',
					'w',
					'l',
					'W',
					'H:i:s',
					's',
					'h:i:s A',
					'A',
					'F',
					'g',
					'G',
					'h',
					'h',
					'M',
					'D',
					'jS',
					'n',
					'j',
					'Y',
					'd',
					'm',
					'H',
					'i',
					's'
				];
				//@formatter:on
				$sFormatForPhpDateFormat = str_replace($aFormatsForMysqlDateFormat, $aFormatsForPhpDateFormat, $sFormatForMysqlDateFormat);
				/** @noinspection PhpUnnecessaryLocalVariableInspection */
				$sRet = date_format($oDate, $sFormatForPhpDateFormat);

				return $sRet;

			case 'TO_DAYS':
				if (count($this->m_aArgs) != 1)
				{
					throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
				}

				// N°5985 - Since PHP 8.1+, a bug fix on \DateTimeInterval for a date anterior to "1937-05-23" now returns a different number of days. The workaround below aim at making the code work with PHP 7.4 => 8.2+
				//
				// $oDate = new DateTimeImmutable('2020-01-02');
				// $oZero = new DateTimeImmutable('1937-05-22');
				// $iRet = (int) $oDate->diff($oZero)->format('%a');
				// echo $iRet."\n"; // 30174 (PHP 8.0) vs 30175 (PHP 8.1+)
				//
				// $oDate = new DateTimeImmutable('2020-01-02');
				// $oZero = new DateTimeImmutable('1937-05-23');
				// $iRet = (int) $oDate->diff($oZero)->format('%a');
				// echo $iRet."\n"; // 30174 (PHP 8.0) vs 30174 (PHP 8.1+)
				//
				// To work around that we take 1970-01-01 as "zero date" and we offset it with the number of days between 1582-01-01 and 1970-01-01.
				// Note that as the "target" date could be between 1582-01-01 and 1970-01-01 we have to format the interval with the "-"/"+" sign in order to correct the number of days.

				$oDate = new DateTime($this->m_aArgs[0]->Evaluate($aArgs));
				$oZero = new DateTime('1970-01-01');
				$iDaysBetween19700101And15800101 = 141713;
				$iRet = (int) $oZero->diff($oDate)->format('%R%a') + 577815 + $iDaysBetween19700101And15800101;
				return $iRet;

			case 'FROM_DAYS':
				if (count($this->m_aArgs) != 1)
				{
					throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
				}
				$iSince1582 = $this->m_aArgs[0]->Evaluate($aArgs) - 577814;
				$oDate = new DateTime("1582-01-01 +$iSince1582 days");
				$sRet = $oDate->format('Y-m-d');
				return $sRet;

			case 'NOW':
				$sRet = date('Y-m-d H:i:s');
				return $sRet;

			case 'CURRENT_DATE':
				$sRet = date('Y-m-d');
				return $sRet;

			case 'DATE_ADD':
				if (count($this->m_aArgs) != 2)
				{
					throw new \Exception("Function {$this->m_sVerb} requires 2 arguments");
				}
				$sStartDate = $this->m_aArgs[0]->Evaluate($aArgs);
				$sInterval = $this->m_aArgs[1]->Evaluate($aArgs);
				$oDate = new DateTime("$sStartDate +$sInterval");
				$sRet = $oDate->format('Y-m-d H:i:s');
				return $sRet;

			case 'DATE_SUB':
				if (count($this->m_aArgs) != 2)
				{
					throw new \Exception("Function {$this->m_sVerb} requires 2 arguments");
				}
				$sStartDate = $this->m_aArgs[0]->Evaluate($aArgs);
				$sInterval = $this->m_aArgs[1]->Evaluate($aArgs);
				$oDate = new DateTime("$sStartDate -$sInterval");
				$sRet = $oDate->format('Y-m-d H:i:s');
				return $sRet;

			default:
				throw new Exception("Function {$this->m_sVerb} cannot be evaluated -unhandled yet");
		}
	}

	/**
	 * {@inheritDoc}
	 * @see Expression::ToJSON()
	 */
	public function ToJSON(&$aArgs = null, $bRetrofitParams = false)
	{
		$aFields = array();
		foreach ($this->m_aArgs as $oExpr)
		{
			$aFields[] = $oExpr->ToJSON($aArgs, $bRetrofitParams);
		}

		return array(
			'type' => static::OPERATOR_FUNCTION,
			'operator' => $this->m_sVerb,
			'fields' => $aFields,
			'oql' => $this->RenderExpression(false, $aArgs, $bRetrofitParams),
		);
	}

	public function Browse(Closure $callback)
	{
		$callback($this);
		foreach ($this->m_aArgs as $iPos => $oExpr)
		{
			$oExpr->Browse($callback);
		}
	}

	public function ApplyParameters($aArgs)
	{
		foreach ($this->m_aArgs as $idx => $oExpr)
		{
			if ($oExpr instanceof VariableExpression)
			{
				$this->m_aArgs[$idx] = $oExpr->GetAsScalar($aArgs);
			}
			else
			{
				$oExpr->ApplyParameters($aArgs);
			}
		}
	}

	public function GetUnresolvedFields($sAlias, &$aUnresolved)
	{
		foreach ($this->m_aArgs as $oExpr)
		{
			$oExpr->GetUnresolvedFields($sAlias, $aUnresolved);
		}
	}

	public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
	{
		$aRes = array();
		foreach ($this->m_aArgs as $oExpr)
		{
			$aRes[] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
		}
		return new FunctionExpression($this->m_sVerb, $aRes);
	}

	public function ListRequiredFields()
	{
		$aRes = array();
		foreach ($this->m_aArgs as $oExpr)
		{
			$aRes = array_merge($aRes, $oExpr->ListRequiredFields());
		}
		return $aRes;
	}

	public function CollectUsedParents(&$aTable)
	{
		foreach ($this->m_aArgs as $oExpr)
		{
			$oExpr->CollectUsedParents($aTable);
		}
	}

	public function ListConstantFields()
	{
		$aRes = array();
		foreach ($this->m_aArgs as $oExpr)
		{
			$aRes = array_merge($aRes, $oExpr->ListConstantFields());
		}
		return $aRes;
	}

	public function ListParameters()
	{

		$aRes = array();
		foreach ($this->m_aArgs as $oExpr)
		{
			$aRes = array_merge($aRes, $oExpr->ListParameters());
		}
		return $aRes;
	}

	public function RenameParam($sOldName, $sNewName)
	{
		foreach ($this->m_aArgs as $key => $oExpr)
		{
			$this->m_aArgs[$key] = $oExpr->RenameParam($sOldName, $sNewName);
		}
	}

	public function RenameAlias($sOldName, $sNewName)
	{
		foreach ($this->m_aArgs as $key => $oExpr)
		{
			$oExpr->RenameAlias($sOldName, $sNewName);
		}
	}

	public function GetAttDef($aClasses = array())
	{
		foreach($this->m_aArgs as $oExpression)
		{
			$oAttDef = $oExpression->GetAttDef($aClasses);
			if (!is_null($oAttDef)) return $oAttDef;
		}

		return null;
	}

	/**
	 * Make the most relevant label, given the value of the expression
	 *
	 * @param DBSearch oFilter The context in which this expression has been used
	 * @param string sValue The value returned by the query, for this expression
	 * @param string sDefault The default value if no relevant label could be computed
	 *
	 * @return string label
	 */
	public function MakeValueLabel($oFilter, $sValue, $sDefault)
	{
		static $aWeekDayToString = null;
		if (is_null($aWeekDayToString)) {
			// Init the correspondance table
			$aWeekDayToString = array(
				0 => Dict::S('DayOfWeek-Sunday'),
				1 => Dict::S('DayOfWeek-Monday'),
				2 => Dict::S('DayOfWeek-Tuesday'),
				3 => Dict::S('DayOfWeek-Wednesday'),
				4 => Dict::S('DayOfWeek-Thursday'),
				5 => Dict::S('DayOfWeek-Friday'),
				6 => Dict::S('DayOfWeek-Saturday'),
			);
		}
		static $aMonthToString = null;
		if (is_null($aMonthToString))
		{
			// Init the correspondance table
			$aMonthToString = array(
				1  => Dict::S('Month-01'),
				2  => Dict::S('Month-02'),
				3  => Dict::S('Month-03'),
				4  => Dict::S('Month-04'),
				5  => Dict::S('Month-05'),
				6  => Dict::S('Month-06'),
				7  => Dict::S('Month-07'),
				8  => Dict::S('Month-08'),
				9  => Dict::S('Month-09'),
				10 => Dict::S('Month-10'),
				11 => Dict::S('Month-11'),
				12 => Dict::S('Month-12'),
			);
		}

		$sRes = $sDefault;
		if (strtolower($this->m_sVerb) == 'date_format') {
			$oFormatExpr = $this->m_aArgs[1];

			if ($oFormatExpr->RenderExpression() == "'%w'") {
				if (isset($aWeekDayToString[(int)$sValue])) {
					$sRes = $aWeekDayToString[(int)$sValue];
				}
			} elseif ($oFormatExpr->RenderExpression() == "'%Y-%m'") {
				// yyyy-mm => "yyyy month"
				$iMonth = (int)substr($sValue, -2); // the two last chars
				$sRes = substr($sValue, 0, 4).' '.$aMonthToString[$iMonth];
			} elseif ($oFormatExpr->RenderExpression() == "'%Y-%m-%d'") {
				// yyyy-mm-dd => "month d"
				$iMonth = (int)substr($sValue, 5, 2);
				$sRes = $aMonthToString[$iMonth].' '.(int)substr($sValue, -2);
			} elseif ($oFormatExpr->RenderExpression() == "'%H'") {
				// H => "H Hour(s)"
				$sRes = $sValue.':00';
			}
		}
		return $sRes;
	}

	public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array())
	{
		$sOperation = '';
		$sVerb = '';
		switch ($this->m_sVerb)
		{
			case 'ISNULL':
			case 'NOW':
				$sVerb = $this->VerbToNaturalLanguage();
				break;
			case 'DATE_SUB':
				$sVerb = ' -';
				break;
			case 'DATE_ADD':
				$sVerb = ' +';
				break;
			case 'DATE_FORMAT':
				$aCtx['date_display'] = $this;
				break;
			default:
				return $this->RenderExpression(false, $aArgs);
		}

		foreach($this->m_aArgs as $oExpression)
		{
			if ($oExpression instanceof IntervalExpression)
			{
				$sOperation .= $sVerb;
				$sVerb = '';
			}
			$sOperation .= $oExpression->Display($oSearch, $aArgs, $oAttDef, $aCtx);
		}

		if (!empty($sVerb))
		{
			$sOperation .= $sVerb;
		}
		return '('.$sOperation.')';
	}

	private function VerbToNaturalLanguage()
	{
		return Dict::S('Expression:Verb:'.$this->m_sVerb, " {$this->m_sVerb} ");
	}

	public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null)
	{
		$aCriteria = array();
		switch ($this->m_sVerb)
		{
			case 'ISNULL':
				$aCriteria['operator'] = $this->m_sVerb;
				foreach($this->m_aArgs as $oExpression)
				{
					$aCriteria = array_merge($oExpression->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef), $aCriteria);
				}
				$aCriteria['has_undefined'] = true;
				$aCriteria['oql'] = $this->RenderExpression(false, $aArgs, $bRetrofitParams);
				break;

			case 'NOW':
				$aCriteria = array('widget' => 'date_time');
				$aCriteria['is_relative'] = true;
				$aCriteria['verb'] = $this->m_sVerb;
				break;

			case 'DATE_ADD':
			case 'DATE_SUB':
			case 'DATE_FORMAT':
				$aCriteria = array('widget' => 'date_time');
				foreach($this->m_aArgs as $oExpression)
				{
					$aCriteria = array_merge($oExpression->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef), $aCriteria);
				}
				$aCriteria['verb'] = $this->m_sVerb;
				break;

			default:
				return parent::GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef);
		}

		return $aCriteria;
	}
}

/**
 * @see https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html
 */
class IntervalExpression extends Expression
{
	protected $m_oValue; // expression
	protected $m_sUnit;

	public function __construct($oValue, $sUnit)
	{
		$this->m_oValue = $oValue;
		$this->m_sUnit = $sUnit;
	}

	public function IsTrue()
	{
		// return true if we are certain that it will be true
		return false;
	}

	public function GetValue()
	{
		return $this->m_oValue;
	}

	public function GetUnit()
	{
		return $this->m_sUnit;
	}

	// recursive rendering
	public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false)
	{
		return 'INTERVAL '.$this->m_oValue->RenderExpression($bForSQL, $aArgs, $bRetrofitParams).' '.$this->m_sUnit;
	}

	/**
	 * Evaluate the value of the expression
	 * @param array $aArgs
	 * @throws \Exception if terms cannot be evaluated as scalars
*/
	public function Evaluate(array $aArgs)
	{
		$iValue = $this->m_oValue->Evaluate($aArgs);
		return "$iValue {$this->m_sUnit}";
	}

	/**
	 * {@inheritDoc}
	 * @see Expression::ToJSON()
	 */
	public function ToJSON(&$aArgs = null, $bRetrofitParams = false)
	{
		return array(
			'type' => static::OPERATOR_INTERVAL,
			'unit' => $this->m_sUnit,
			'expression' => $this->m_oValue->ToJSON($aArgs, $bRetrofitParams),
			'oql' => $this->RenderExpression(false, $aArgs, $bRetrofitParams),
		);
	}

	public function Browse(Closure $callback)
	{
		$callback($this);
		$this->m_oValue->Browse($callback);
	}

	public function ApplyParameters($aArgs)
	{
		if ($this->m_oValue instanceof VariableExpression)
		{
			$this->m_oValue = $this->m_oValue->GetAsScalar($aArgs);
		}
		else
		{
			$this->m_oValue->ApplyParameters($aArgs);
		}
	}

	public function GetUnresolvedFields($sAlias, &$aUnresolved)
	{
		$this->m_oValue->GetUnresolvedFields($sAlias, $aUnresolved);
	}

	public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
	{
		return new IntervalExpression($this->m_oValue->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved), $this->m_sUnit);
	}

	public function ListRequiredFields()
	{
		return array();
	}

	public function CollectUsedParents(&$aTable)
	{
	}

	public function ListConstantFields()
	{
		return array();
	}

	public function ListParameters()
	{
		return $this->m_oValue->ListParameters();
	}

	public function RenameParam($sOldName, $sNewName)
	{
		$this->m_oValue->RenameParam($sOldName, $sNewName);
	}

	public function RenameAlias($sOldName, $sNewName)
	{
		$this->m_oValue->RenameAlias($sOldName, $sNewName);
	}

	public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null)
	{
		$aCriteria = $this->m_oValue->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef);
		$aCriteria['unit'] = $this->m_sUnit;

		return $aCriteria;
	}

	public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array())
	{
		return $this->m_oValue->RenderExpression(false, $aArgs).' '.Dict::S('Expression:Unit:Long:'.$this->m_sUnit, $this->m_sUnit);
	}
}

class CharConcatExpression extends Expression
{
	protected $m_aExpressions;

	public function __construct($aExpressions)
	{
		$this->m_aExpressions = $aExpressions;
	}

	public function IsTrue()
	{
		// return true if we are certain that it will be true
		return false;
	}

	public function GetItems()
	{
		return $this->m_aExpressions;
	}

	// recursive rendering
	public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false)
	{
		$aRes = array();
		foreach ($this->m_aExpressions as $oExpr)
		{
			$sCol = $oExpr->RenderExpression($bForSQL, $aArgs, $bRetrofitParams);
			// Concat will be globally NULL if one single argument is null !
			$aRes[] = "COALESCE($sCol, '')";
		}
		return "CAST(CONCAT(".implode(', ', $aRes).") AS CHAR)";
	}

	/**
	 * Evaluate the value of the expression
	 * @param array $aArgs
	 * @throws \Exception if terms cannot be evaluated as scalars
*/
	public function Evaluate(array $aArgs)
	{
		$sRet = '';
		foreach ($this->m_aExpressions as $oExpr)
		{
			$sRet .= $oExpr->Evaluate($aArgs);
		}
		return $sRet;
	}

	/**
	 * {@inheritDoc}
	 * @see Expression::ToJSON()
	 */
	public function ToJSON(&$aArgs = null, $bRetrofitParams = false)
	{
		$aFields = array();
		foreach ($this->m_aExpressions as $oExpr)
		{
			$aFields[] = $oExpr->RenderExpression($aArgs, $bRetrofitParams);
		}

		return array(
			'type' => static::OPERATOR_FUNCTION,
			'operator' => 'concat',
			'fields' => $aFields,
		);
	}

	public function Browse(Closure $callback)
	{
		$callback($this);
		foreach ($this->m_aExpressions as $oExpr)
		{
			$oExpr->Browse($callback);
		}
	}

	public function ApplyParameters($aArgs)
	{
		foreach ($this->m_aExpressions as $idx => $oExpr)
		{
			if ($oExpr instanceof VariableExpression)
			{
				$this->m_aExpressions[$idx] = $oExpr->GetAsScalar($aArgs);
			}
			else
			{
				$this->m_aExpressions->ApplyParameters($aArgs);
			}
		}
	}

	public function GetUnresolvedFields($sAlias, &$aUnresolved)
	{
		foreach ($this->m_aExpressions as $oExpr)
		{
			$oExpr->GetUnresolvedFields($sAlias, $aUnresolved);
		}
	}

	public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
	{
		$aRes = array();
		foreach ($this->m_aExpressions as $oExpr)
		{
			$aRes[] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
		}
		return new CharConcatExpression($aRes);
	}

	public function ListRequiredFields()
	{
		$aRes = array();
		foreach ($this->m_aExpressions as $oExpr)
		{
			$aRes = array_merge($aRes, $oExpr->ListRequiredFields());
		}
		return $aRes;
	}

	public function CollectUsedParents(&$aTable)
	{
		foreach ($this->m_aExpressions as $oExpr)
		{
			$oExpr->CollectUsedParents($aTable);
		}
	}

	public function ListConstantFields()
	{
		$aRes = array();
		foreach ($this->m_aExpressions as $oExpr)
		{
			$aRes = array_merge($aRes, $oExpr->ListConstantFields());
		}
		return $aRes;
	}

	public function ListParameters()
	{
		$aRes = array();
		foreach ($this->m_aExpressions as $oExpr)
		{
			$aRes = array_merge($aRes, $oExpr->ListParameters());
		}
		return $aRes;
	}

	public function RenameParam($sOldName, $sNewName)
	{
		foreach ($this->m_aExpressions as $key => $oExpr)
		{
			$this->m_aExpressions[$key] = $oExpr->RenameParam($sOldName, $sNewName);
		}
	}

	public function RenameAlias($sOldName, $sNewName)
	{
		foreach ($this->m_aExpressions as $key => $oExpr)
		{
			$oExpr->RenameAlias($sOldName, $sNewName);
		}
	}
}


class CharConcatWSExpression extends CharConcatExpression
{
	protected $m_separator;

	public function __construct($separator, $aExpressions)
	{
		$this->m_separator = $separator;
		parent::__construct($aExpressions);
	}

	// recursive rendering
	public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false)
	{
		$aRes = array();
		foreach ($this->m_aExpressions as $oExpr)
		{
			$sCol = $oExpr->RenderExpression($bForSQL, $aArgs, $bRetrofitParams);
			// Concat will be globally NULL if one single argument is null !
			$aRes[] = "COALESCE($sCol, '')";
		}
		$sSep = CMDBSource::Quote($this->m_separator);
		return "CAST(CONCAT_WS($sSep, ".implode(', ', $aRes).") AS CHAR)";
	}

	/**
	 * Evaluate the value of the expression
	 * @param array $aArgs
	 * @throws \Exception if terms cannot be evaluated as scalars
*/
	public function Evaluate(array $aArgs)
	{
		$aRes = array();
		foreach ($this->m_aExpressions as $oExpr)
		{
			// TODO: Seems weird, this should rather be $aRes[] = $oExpr->Evaluate($aArgs);
			$aRes .= $oExpr->Evaluate($aArgs);
		}
		return implode($this->m_separator, $aRes);
	}

	public function Browse(Closure $callback)
	{
		$callback($this);
		foreach ($this->m_aExpressions as $oExpr)
		{
			$oExpr->Browse($callback);
		}
	}

	public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
	{
		$aRes = array();
		foreach ($this->m_aExpressions as $oExpr)
		{
			$aRes[] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
		}
		return new CharConcatWSExpression($this->m_separator, $aRes);
	}
}

