<?php

/**
 * LimeSurvey
 * Copyright (C) 2007-2013 The LimeSurvey Project Team / Carsten Schmitz
 * All rights reserved.
 * License: GNU/GPL License v2 or later, see LICENSE.php
 * LimeSurvey is free software. This version may have been modified pursuant
 * to the GNU General Public License, and as distributed it includes or
 * is derivative of works licensed under the GNU General Public License or
 * other free or open source software licenses.
 * See COPYRIGHT.php for copyright notices and details.
 *
 */
/**
 * Description of ExpressionManager
 * (1) Does safe evaluation of PHP expressions.  Only registered Functions, and known Variables are allowed.
 *   (a) Functions include any math, string processing, conditional, formatting, etc. functions
 * (2) This class replaces LimeSurvey's <= 1.91+  process of resolving strings that contain LimeReplacementFields
 *   (a) String is split by expressions (by curly braces, but safely supporting strings and escaped curly braces)
 *   (b) Expressions (things surrounded by curly braces) are evaluated - thereby doing LimeReplacementField substitution and/or more complex calculations
 *   (c) Non-expressions are left intact
 *   (d) The array of stringParts are re-joined to create the desired final string.
 * (3) The core of ExpressionScript Engine is a Recursive Descent Parser (RDP), based off of one build via JavaCC by TMSWhite in 1999.
 *   (a) Functions that start with RDP_ should not be touched unless you really understand compiler design.
 *
 * @author LimeSurvey Team (limesurvey.org)
 * @author Thomas M. White (TMSWhite)
 */

class ExpressionManager
{
    // These are the allowable variable suffixes for variables - each represents an attribute of a variable that can be updated on same page
    private $aRDP_regexpVariableAttribute = array(
        'code',
        'NAOK',
        'relevanceStatus',
        'shown',
        'valueNAOK',
        'value',
    );
    /* var string[] allowable static suffixes for variables - each represents an attribute of a variable that can not be updated on same page
     * @see LimeExpressionManager->knownVars definition
     */
    private $aRDP_regexpStaticAttribute = array(
        'qid',
        'gid',
        'question',
        'sgqa',
        'type',
        'relevance',
        'grelevance',
        'qseq',
        'gseq',
        'jsName',
        'jsName_on',
        'mandatory',
        'rowdivid',
    );
    // These three variables are effectively static once constructed
    private $RDP_ExpressionRegex;
    private $RDP_TokenType;
    private $RDP_TokenizerRegex;
    private $RDP_CategorizeTokensRegex;
    private $RDP_ValidFunctions; // names and # params of valid functions

    // Thes variables are used while  processing the equation
    private $RDP_expr; // the source expression
    private $RDP_tokens; // the list of generated tokens
    private $RDP_count; // total number of $RDP_tokens
    private $RDP_pos; // position within the $token array while processing equation
    /** @var array[] information about current errors : array with string, $token (EM internal array). Reset in RDP_Evaluate (and only in RDP_Evaluate) */
    private $RDP_errs;
    /** @var array[] information about current warnings : array with string, $token (EM internal array) and optional link Reset in RDP_Evaluate or manually */
    private $RDP_warnings = array();
    private $RDP_onlyparse;
    private $RDP_stack; // stack of intermediate results
    private $RDP_result; // final result of evaluating the expression;
    private $RDP_evalStatus; // true if $RDP_result is a valid result, and  there are no serious errors
    private $varsUsed; // list of variables referenced in the equation
    public $resetErrorsAndWarningsOnEachPart = true;

    // These  variables are only used by sProcessStringContainingExpressions
    private $allVarsUsed; // full list of variables used within the string, even if contains multiple expressions
    private $prettyPrintSource; // HTML formatted output of running sProcessStringContainingExpressions
    private $substitutionNum; // Keeps track of number of substitions performed XXX

    /**
     * @var array
     */
    private $substitutionInfo; // array of JavaScripts to managing dynamic substitution
    private $jsExpression; // caches computation of JavaScript equivalent for an Expression

    private $questionSeq; // sequence order of question - so can detect if try to use variable before it is set
    private $groupSeq; // sequence order of groups - so can detect if try to use variable before it is set
    private $surveyMode = 'group';

    // The following are only needed to enable click on variable names within pretty print and open new window to edit them
    private $sid = null; // the survey ID
    private $hyperlinkSyntaxHighlighting = true; // TODO - change this back to false
    private $sgqaNaming = false;

    function __construct()
    {
        /* EM core string must be in adminlang : keep the actual for resetting at end. See bug #12208 */
        /**
         * @var string|null $baseLang set the previous language if need to be set
         */
        $baseLang = null;
        if (Yii::app() instanceof CWebApplication && Yii::app()->session['adminlang']) {
            $baseLang = Yii::app()->getLanguage();
            Yii::app()->setLanguage(Yii::app()->session['adminlang']);
        }
        // List of token-matching regular expressions
        // Note, this is effectively a Lexer using Regular Expressions.  Don't change this unless you understand compiler design.
        $RDP_regex_dq_string = '(?<!\\\\)".*?(?<!\\\\)"';
        $RDP_regex_sq_string = '(?<!\\\\)\'.*?(?<!\\\\)\'';
        $RDP_regex_whitespace = '\s+';
        $RDP_regex_lparen = '\(';
        $RDP_regex_rparen = '\)';
        $RDP_regex_comma = ',';
        $RDP_regex_not = '!';
        $RDP_regex_inc_dec = '\+\+|--';
        $RDP_regex_binary = '[+*/-]';
        $RDP_regex_compare = '<=|<|>=|>|==|!=|\ble\b|\blt\b|\bge\b|\bgt\b|\beq\b|\bne\b';
        $RDP_regex_assign = '='; // '=|\+=|-=|\*=|/=';
        $RDP_regex_sgqa = '(?:INSERTANS:)?[0-9]+X[0-9]+X[0-9]+[A-Z0-9_]*\#?[01]?(?:\.(?:[a-zA-Z0-9_]*))?';
        $RDP_regex_word = '(?:TOKEN:)?(?:[A-Z][A-Z0-9_]*)?(?:\.(?:[A-Z][A-Z0-9_]*))*(?:\.(?:[a-zA-Z0-9_]*))?';
        $RDP_regex_number = '[0-9]+\.?[0-9]*|\.[0-9]+';
        $RDP_regex_andor = '\band\b|\bor\b|&&|\|\|';
        $RDP_regex_lcb = '{';
        $RDP_regex_rcb = '}';
        $RDP_regex_sq = '\'';
        $RDP_regex_dq = '"';
        $RDP_regex_bs = '\\\\';

        $RDP_StringSplitRegex = array(
            $RDP_regex_lcb,
            $RDP_regex_rcb,
            $RDP_regex_sq,
            $RDP_regex_dq,
            $RDP_regex_bs,
        );

        // RDP_ExpressionRegex is the regular expression that splits apart strings that contain curly braces in order to find expressions
        $this->RDP_ExpressionRegex = '#(' . implode('|', $RDP_StringSplitRegex) . ')#i';

        // asTokenRegex and RDP_TokenType must be kept in sync  (same number and order)
        $RDP_TokenRegex = array(
            $RDP_regex_dq_string,
            $RDP_regex_sq_string,
            $RDP_regex_whitespace,
            $RDP_regex_lparen,
            $RDP_regex_rparen,
            $RDP_regex_comma,
            $RDP_regex_andor,
            $RDP_regex_compare,
            $RDP_regex_sgqa,
            $RDP_regex_word,
            $RDP_regex_number,
            $RDP_regex_not,
            $RDP_regex_inc_dec,
            $RDP_regex_assign,
            $RDP_regex_binary,
            );

        $this->RDP_TokenType = array(
            'DQ_STRING',
            'SQ_STRING',
            'SPACE',
            'LP',
            'RP',
            'COMMA',
            'AND_OR',
            'COMPARE',
            'SGQA',
            'WORD',
            'NUMBER',
            'NOT',
            'OTHER',
            'ASSIGN',
            'BINARYOP',
            );

        // $RDP_TokenizerRegex - a single regex used to split and equation into tokens
        $this->RDP_TokenizerRegex = '#(' . implode('|', $RDP_TokenRegex) . ')#i';

        // $RDP_CategorizeTokensRegex - an array of patterns so can categorize the type of token found - would be nice if could get this from preg_split
        // Adding ability to capture 'OTHER' type, which indicates an error - unsupported syntax element
        $this->RDP_CategorizeTokensRegex = preg_replace("#^(.*)$#", "#^$1$#i", $RDP_TokenRegex);
        $this->RDP_CategorizeTokensRegex[] = '/.+/';
        $this->RDP_TokenType[] = 'OTHER';
        // Each allowed function is a mapping from local name to external name + number of arguments
        // Functions can have a list of serveral allowable #s of arguments.
        // If the value is -1, the function must have a least one argument but can have an unlimited number of them
        // -2 means that at least one argument is required.  -3 means at least two arguments are required, etc.
        $this->RDP_ValidFunctions = array(
            'abs' => array('exprmgr_abs', 'Decimal.asNum.abs', gT('Absolute value'), 'number abs(number)', 'http://php.net/abs', 1),
            'acos' => array('acos', 'Decimal.asNum.acos', gT('Arc cosine'), 'number acos(number)', 'http://php.net/acos', 1),
            'addslashes' => array('addslashes', gT('addslashes'), 'Quote string with slashes', 'string addslashes(string)', 'http://php.net/addslashes', 1),
            'asin' => array('asin', 'Decimal.asNum.asin', gT('Arc sine'), 'number asin(number)', 'http://php.net/asin', 1),
            'atan' => array('atan', 'Decimal.asNum.atan', gT('Arc tangent'), 'number atan(number)', 'http://php.net/atan', 1),
            'atan2' => array('atan2', 'Decimal.asNum.atan2', gT('Arc tangent of two variables'), 'number atan2(number, number)', 'http://php.net/atan2', 2),
            'ceil' => array('ceil', 'Decimal.asNum.ceil', gT('Round fractions up'), 'number ceil(number)', 'http://php.net/ceil', 1),
            'checkdate' => array('exprmgr_checkdate', 'checkdate', gT('Returns true(1) if it is a valid date in gregorian calendar'), 'bool checkdate(month,day,year)', 'http://php.net/checkdate', 3),
            'cos' => array('cos', 'Decimal.asNum.cos', gT('Cosine'), 'number cos(number)', 'http://php.net/cos', 1),
            'count' => array('exprmgr_count', 'LEMcount', gT('Count the number of answered questions in the list'), 'number count(arg1, arg2, ... argN)', '', -1),
            'countif' => array('exprmgr_countif', 'LEMcountif', gT('Count the number of answered questions in the list equal the first argument'), 'number countif(matches, arg1, arg2, ... argN)', '', -2),
            'countifop' => array('exprmgr_countifop', 'LEMcountifop', gT('Count the number of answered questions in the list which pass the critiera (arg op value)'), 'number countifop(op, value, arg1, arg2, ... argN)', '', -3),
            'date' => array('exprmgr_date', 'date', gT('Format a local date/time'), 'string date(format [, timestamp=time()])', 'http://php.net/date', 1, 2),
            'exp' => array('exp', 'Decimal.asNum.exp', gT('Calculates the exponent of e'), 'number exp(number)', 'http://php.net/exp', 1),
            'fixnum' => array('exprmgr_fixnum', 'LEMfixnum', gT('Display numbers with comma as decimal separator, if needed'), 'string fixnum(number)', '', 1),
            'floatval' => array('floatval', 'LEMfloatval', gT('Get float value of a variable'), 'number floatval(number)', 'http://php.net/floatval', 1),
            'floor' => array('floor', 'Decimal.asNum.floor', gT('Round fractions down'), 'number floor(number)', 'http://php.net/floor', 1),
            'gmdate' => array('gmdate', 'gmdate', gT('Format a GMT date/time'), 'string gmdate(format [, timestamp=time()])', 'http://php.net/gmdate', 1, 2),
            'html_entity_decode' => array('html_entity_decode', 'html_entity_decode', gT('Convert all HTML entities to their applicable characters (always uses ENT_QUOTES and UTF-8)'), 'string html_entity_decode(string)', 'http://php.net/html-entity-decode', 1),
            'htmlentities' => array('htmlentities', 'htmlentities', gT('Convert all applicable characters to HTML entities (always uses ENT_QUOTES and UTF-8)'), 'string htmlentities(string)', 'http://php.net/htmlentities', 1),
            'htmlspecialchars' => array('expr_mgr_htmlspecialchars', 'htmlspecialchars', gT('Convert special characters to HTML entities (always uses ENT_QUOTES and UTF-8)'), 'string htmlspecialchars(string)', 'http://php.net/htmlspecialchars', 1),
            'htmlspecialchars_decode' => array('expr_mgr_htmlspecialchars_decode', 'htmlspecialchars_decode', gT('Convert special HTML entities back to characters (always uses ENT_QUOTES and UTF-8)'), 'string htmlspecialchars_decode(string)', 'http://php.net/htmlspecialchars-decode', 1),
            'idate' => array('idate', 'idate', gT('Format a local time/date as integer'), 'string idate(string [, timestamp=time()])', 'http://php.net/idate', 1, 2),
            'if' => array('exprmgr_if', 'LEMif', gT('Conditional processing'), 'if(test,result_if_true[,result_if_false = \'\'])', '', 2, 3),
            'implode' => array('exprmgr_implode', 'LEMimplode', gT('Join array elements with a string'), 'string implode(glue,arg1,arg2,...,argN)', 'http://php.net/implode', -2),
            'intval' => array('intval', 'LEMintval', gT('Get the integer value of a variable'), 'int intval(number [, base=10])', 'http://php.net/intval', 1, 2),
            'is_empty' => array('exprmgr_empty', 'LEMempty', gT('Determine whether a variable is considered to be empty'), 'bool is_empty(var)', 'http://php.net/empty', 1),
            'is_float' => array('is_float', 'LEMis_float', gT('Finds whether the type of a variable is float'), 'bool is_float(var)', 'http://php.net/is-float', 1),
            'is_int' => array('exprmgr_int', 'LEMis_int', gT('Check if the content of a variable is a valid integer value'), 'bool is_int(var)', 'http://php.net/is-int', 1),
            'is_nan' => array('is_nan', 'isNaN', gT('Finds whether a value is not a number'), 'bool is_nan(var)', 'http://php.net/is-nan', 1),
            'is_null' => array('is_null', 'LEMis_null', gT('Finds whether a variable is NULL'), 'bool is_null(var)', 'http://php.net/is-null', 1),
            'is_numeric' => array('is_numeric', 'LEMis_numeric', gT('Finds whether a variable is a number or a numeric string'), 'bool is_numeric(var)', 'http://php.net/is-numeric', 1),
            'is_string' => array('is_string', 'LEMis_string', gT('Find whether the type of a variable is string'), 'bool is_string(var)', 'http://php.net/is-string', 1),
            'join' => array('exprmgr_join', 'LEMjoin', gT('Join strings, return joined string.This function is an alias of implode("",argN)'), 'string join(arg1,arg2,...,argN)', '', -1),
            'list' => array('exprmgr_list', 'LEMlist', gT('Return comma-separated list of values'), 'string list(arg1, arg2, ... argN)', '', -2),
            'listifop' => array('exprmgr_listifop', 'LEMlistifop', gT('Return a list of retAttr from sgqa1...sgqaN which pass the criteria (cmpAttr op value)'), 'string listifop(cmpAttr, op, value, retAttr, glue, sgqa1, sgqa2,...,sgqaN)', '', -6),
            'log' => array('exprmgr_log', 'LEMlog', gT('The logarithm of number to base, if given, or the natural logarithm. '), 'number log(number,base=e)', 'http://php.net/log', -2),
            'ltrim' => array('ltrim', 'ltrim', gT('Strip whitespace (or other characters) from the beginning of a string'), 'string ltrim(string [, charlist])', 'http://php.net/ltrim', 1, 2),
            'max' => array('max', 'LEMmax', gT('Find highest value'), 'number|string max(arg1, arg2, ... argN)', 'http://php.net/max', -2),
            'min' => array('min', 'LEMmin', gT('Find lowest value'), 'number|string min(arg1, arg2, ... argN)', 'http://php.net/min', -2),
            'mktime' => array('exprmgr_mktime', 'mktime', gT('Get UNIX timestamp for a date (each of the 6 arguments are optional)'), 'number mktime([hour [, minute [, second [, month [, day [, year ]]]]]])', 'http://php.net/mktime', 0, 1, 2, 3, 4, 5, 6),
            'nl2br' => array('nl2br', 'nl2br', gT('Inserts HTML line breaks before all newlines in a string'), 'string nl2br(string)', 'http://php.net/nl2br', 1, 1),
            'number_format' => array('number_format', 'number_format', gT('Format a number with grouped thousands'), 'string number_format(number)', 'http://php.net/number-format', 1),
            'pi' => array('pi', 'LEMpi', gT('Get value of pi'), 'number pi()', '', 0),
            'pow' => array('pow', 'Decimal.asNum.pow', gT('Exponential expression'), 'number pow(base, exp)', 'http://php.net/pow', 2),
            'quoted_printable_decode' => array('quoted_printable_decode', 'quoted_printable_decode', gT('Convert a quoted-printable string to an 8 bit string'), 'string quoted_printable_decode(string)', 'http://php.net/quoted-printable-decode', 1),
            'quoted_printable_encode' => array('quoted_printable_encode', 'quoted_printable_encode', gT('Convert a 8 bit string to a quoted-printable string'), 'string quoted_printable_encode(string)', 'http://php.net/quoted-printable-encode', 1),
            'quotemeta' => array('quotemeta', 'quotemeta', gT('Quote meta characters'), 'string quotemeta(string)', 'http://php.net/quotemeta', 1),
            'rand' => array('rand', 'rand', gT('Generate a random integer'), 'int rand() OR int rand(min, max)', 'http://php.net/rand', 0, 2),
            'regexMatch' => array('exprmgr_regexMatch', 'LEMregexMatch', gT('Compare a string to a regular expression pattern'), 'bool regexMatch(pattern,input)', '', 2),
            'round' => array('round', 'round', gT('Rounds a number to an optional precision'), 'number round(val [, precision])', 'http://php.net/round', 1, 2),
            'rtrim' => array('rtrim', 'rtrim', gT('Strip whitespace (or other characters) from the end of a string'), 'string rtrim(string [, charlist])', 'http://php.net/rtrim', 1, 2),
            'sin' => array('sin', 'Decimal.asNum.sin', gT('Sine'), 'number sin(arg)', 'http://php.net/sin', 1),
            'sprintf' => array('sprintf', 'sprintf', gT('Return a formatted string'), 'string sprintf(format, arg1, arg2, ... argN)', 'http://php.net/sprintf', -2),
            'sqrt' => array('sqrt', 'Decimal.asNum.sqrt', gT('Square root'), 'number sqrt(arg)', 'http://php.net/sqrt', 1),
            'stddev' => array('exprmgr_stddev', 'LEMstddev', gT('Calculate the Sample Standard Deviation for the list of numbers'), 'number stddev(arg1, arg2, ... argN)', '', -2),
            'str_pad' => array('str_pad', 'str_pad', gT('Pad a string to a certain length with another string'), 'string str_pad(input, pad_length [, pad_string])', 'http://php.net/str-pad', 2, 3),
            'str_repeat' => array('str_repeat', 'str_repeat', gT('Repeat a string'), 'string str_repeat(input, multiplier)', 'http://php.net/str-repeat', 2),
            'str_replace' => array('str_replace', 'LEMstr_replace', gT('Replace all occurrences of the search string with the replacement string'), 'string str_replace(search,  replace, subject)', 'http://php.net/str-replace', 3),
            'strcasecmp' => array('strcasecmp', 'strcasecmp', gT('Binary safe case-insensitive string comparison'), 'int strcasecmp(str1, str2)', 'http://php.net/strcasecmp', 2),
            'strcmp' => array('strcmp', 'strcmp', gT('Binary safe string comparison'), 'int strcmp(str1, str2)', 'http://php.net/strcmp', 2),
            'strip_tags' => array('strip_tags', 'strip_tags', gT('Strip HTML and PHP tags from a string'), 'string strip_tags(str, allowable_tags)', 'http://php.net/strip-tags', 1, 2),
            'stripos' => array('exprmgr_stripos', 'stripos', gT('Find position of first occurrence of a case-insensitive string'), 'int stripos(haystack, needle [, offset=0])', 'http://php.net/stripos', 2, 3),
            'stripslashes' => array('stripslashes', 'stripslashes', gT('Un-quotes a quoted string'), 'string stripslashes(string)', 'http://php.net/stripslashes', 1),
            'stristr' => array('exprmgr_stristr', 'stristr', gT('Case-insensitive strstr'), 'string stristr(haystack, needle [, before_needle=false])', 'http://php.net/stristr', 2, 3),
            'strlen' => array('exprmgr_strlen', 'LEMstrlen', gT('Get string length'), 'int strlen(string)', 'http://php.net/strlen', 1),
            'strpos' => array('exprmgr_strpos', 'LEMstrpos', gT('Find position of first occurrence of a string'), 'int strpos(haystack, needle [ offset=0])', 'http://php.net/strpos', 2, 3),
            'strrev' => array('strrev', 'strrev', gT('Reverse a string'), 'string strrev(string)', 'http://php.net/strrev', 1),
            'strstr' => array('exprmgr_strstr', 'strstr', gT('Find first occurrence of a string'), 'string strstr(haystack, needle [, before_needle=false])', 'http://php.net/strstr', 2, 3),
            'strtolower' => array('exprmgr_strtolower', 'LEMstrtolower', gT('Make a string lowercase'), 'string strtolower(string)', 'http://php.net/strtolower', 1),
            'strtotime' => array('strtotime', 'strtotime', gT('Convert a date/time string to unix timestamp'), 'int strtotime(string)', 'http://php.net/manual/de/function.strtotime', 1),
            'strtoupper' => array('exprmgr_strtoupper', 'LEMstrtoupper', gT('Make a string uppercase'), 'string strtoupper(string)', 'http://php.net/strtoupper', 1),
            'substr' => array('exprmgr_substr', 'substr', gT('Return part of a string'), 'string substr(string, start [, length])', 'http://php.net/substr', 2, 3),
            'sum' => array('exprmgr_array_sum', 'LEMsum', gT('Calculate the sum of values in an array'), 'number sum(arg1, arg2, ... argN)', '', -2),
            'sumifop' => array('exprmgr_sumifop', 'LEMsumifop', gT('Sum the values of answered questions in the list which pass the critiera (arg op value)'), 'number sumifop(op, value, arg1, arg2, ... argN)', '', -3),
            'tan' => array('tan', 'Decimal.asNum.tan', gT('Tangent'), 'number tan(arg)', 'http://php.net/tan', 1),
            'convert_value' => array('exprmgr_convert_value', 'LEMconvert_value', gT('Convert a numerical value using a inputTable and outputTable of numerical values'), 'number convert_value(fValue, iStrict, sTranslateFromList, sTranslateToList)', '', 4),
            'time' => array('time', 'time', gT('Return current UNIX timestamp'), 'number time()', 'http://php.net/time', 0),
            'trim' => array('trim', 'trim', gT('Strip whitespace (or other characters) from the beginning and end of a string'), 'string trim(string [, charlist])', 'http://php.net/trim', 1, 2),
            'ucwords' => array('ucwords', 'ucwords', gT('Uppercase the first character of each word in a string'), 'string ucwords(string)', 'http://php.net/ucwords', 1),
            'unique' => array('exprmgr_unique', 'LEMunique', gT('Returns true if all non-empty responses are unique'), 'boolean unique(arg1, ..., argN)', '', -1),
        );
        /* Reset the language */
        if ($baseLang) {
            Yii::app()->setLanguage($baseLang);
        }
    }

    /**
     * Since this class can be get by session, need to add a call the «start» event manually
     * @return void
     */
    public function ExpressionManagerStartEvent()
    {
        if (Yii::app() instanceof CConsoleApplication) {
            return;
        }

        $event = new \LimeSurvey\PluginManager\PluginEvent('ExpressionManagerStart');
        $result = App()->getPluginManager()->dispatchEvent($event);
        $newValidFunctions = $result->get('functions', array());
        $newPackages = $result->get('packages', array()); // package added to expression-extend['depends'] : maybe don't add it in event, but add an helper ?

        $this->RegisterFunctions($newValidFunctions); // No validation : plugin dev can break all easily
        foreach ($newPackages as $name => $definition) {
            $this->addPackageForExpressionManager($name, $definition);
        }
        App()->getClientScript()->registerPackage('expression-extend');
    }

    /**
     * Add a package for expression
     * @param string $name of package
     * @param array $definition @see https://www.yiiframework.com/doc/api/1.1/CClientScript#packages-detail
     * @return void
     */
    public function addPackageForExpressionManager($name, $definition)
    {
        Yii::app()->clientScript->addPackage($name, $definition);
        array_push(Yii::app()->clientScript->packages['expression-extend']['depends'], $name);
    }

    /**
     * Add an error to the error log
     *
     * @param string $errMsg
     * @param array|null $token
     * @return void
     */
    private function RDP_AddError($errMsg, $token)
    {
        $this->RDP_errs[] = array($errMsg, $token);
    }

    /**
     * Add a warning to the error log
     *
     * @param EMWarningInterface $warning
     * @return void
     */
    private function RDP_AddWarning(EMWarningInterface $warning)
    {
        $this->RDP_warnings[] = $warning;
    }

    /**
     * @return array
     */
    public function RDP_GetErrors()
    {
        return $this->RDP_errs;
    }

    /**
     * Get informatin about type mismatch between arguments.
     * @param Token $arg1
     * @param Token $arg2
     * @return boolean[] Like (boolean $bMismatchType, boolean $bBothNumeric, boolean $bBothString)
     */
    private function getMismatchInformation(array $arg1, array $arg2)
    {
        /* When value come from DB : it's set to 1.000000 (DECIMAL) : must be fixed see #11163. Response::model() must fix this . or not ? */
        /* Don't return true always : user can entre non numeric value in a numeric value : we must compare as string then */
        $arg1[0] = ($arg1[2] == "NUMBER" && strpos((string) $arg1[0], ".")) ? rtrim(rtrim((string) $arg1[0], "0"), ".") : $arg1[0];
        $arg2[0] = ($arg2[2] == "NUMBER" && strpos((string) $arg2[0], ".")) ? rtrim(rtrim((string) $arg2[0], "0"), ".") : $arg2[0];

        $bNumericArg1 = $arg1[0] !== "" && (!$arg1[0] || strval(floatval($arg1[0])) == strval($arg1[0]));
        $bNumericArg2 = $arg2[0] !== "" && (!$arg2[0] || strval(floatval($arg2[0])) == strval($arg2[0]));
        $bStringArg1 = !$arg1[0] || !$bNumericArg1;
        $bStringArg2 = !$arg2[0] || !$bNumericArg2;

        $bBothNumeric = ($bNumericArg1 && $bNumericArg2);
        $bBothString = ($bStringArg1 && $bStringArg2);
        $bMismatchType = (!$bBothNumeric && !$bBothString);

        return array($bMismatchType, $bBothNumeric, $bBothString);
    }

    /**
     * RDP_EvaluateBinary() computes binary expressions, such as (a or b), (c * d), popping  the top two entries off the
     * stack and pushing the result back onto the stack.
     *
     * @param array $token
     * @return boolean - false if there is any error, else true
     */
    public function RDP_EvaluateBinary(array $token)
    {
        if (count($this->RDP_stack) < 2) {
            $this->RDP_AddError(self::gT("Unable to evaluate binary operator - fewer than 2 entries on stack"), $token);
            return false;
        }
        $arg2 = $this->RDP_StackPop();
        $arg1 = $this->RDP_StackPop();
        if (is_null($arg1) or is_null($arg2)) {
            $this->RDP_AddError(self::gT("Invalid value(s) on the stack"), $token);
            return false;
        }
        list($bMismatchType, $bBothNumeric, $bBothString) = $this->getMismatchInformation($arg1, $arg2);
        $isForcedString = false;
        /* @var array argument as forced string, arg type is at 2.
         * Question can return NUMBER or WORD : DQ and SQ is string entered by user, STRING is WORD with +""
         */
        $aForceStringArray = array('DQ_STRING', 'SQ_STRING', 'STRING'); //
        if (in_array($arg1[2], $aForceStringArray) || in_array($arg2[2], $aForceStringArray)) {
            $isForcedString = true;
            // Set bBothString if one is forced to be string, only if both can be numeric. Mimic JS and PHP
            if ($bBothNumeric) {
                $bBothNumeric = false;
                $bBothString = true;
                $bMismatchType = false;
                $arg1[0] = strval($arg1[0]);
                $arg2[0] = strval($arg2[0]);
            }
        }

        switch (strtolower((string) $token[0])) {
            case 'or':
            case '||':
                $result = array(($arg1[0] or $arg2[0]), $token[1], 'NUMBER');
                break;
            case 'and':
            case '&&':
                $result = array(($arg1[0] and $arg2[0]), $token[1], 'NUMBER');
                break;
            case '==':
            case 'eq':
                $result = array(($arg1[0] == $arg2[0]), $token[1], 'NUMBER');
                break;
            case '!=':
            case 'ne':
                $result = array(($arg1[0] != $arg2[0]), $token[1], 'NUMBER');
                break;
            case '<':
            case 'lt':
                if ($bMismatchType) {
                    if ($isForcedString) {
                        $this->RDP_AddWarning(new EMWarningInvalidComparison($token));
                    }
                    $result = array(false, $token[1], 'NUMBER');
                } elseif (!$bBothNumeric && $bBothString) {
                    if ($isForcedString) {
                        $this->RDP_AddWarning(new EMWarningInvalidComparison($token));
                    }
                    $result = array(strcmp((string) $arg1[0], (string) $arg2[0]) < 0, $token[1], 'NUMBER');
                } else {
                    $result = array(($arg1[0] < $arg2[0]), $token[1], 'NUMBER');
                }
                break;
            case '<=';
            case 'le':
                if ($bMismatchType) {
                    if ($isForcedString) {
                        $this->RDP_AddWarning(new EMWarningInvalidComparison($token));
                    }
                    $result = array(false, $token[1], 'NUMBER');
                } else {
                    // Need this explicit comparison in order to be in agreement with JavaScript
                    if (($arg1[0] == '0' && $arg2[0] == '') || ($arg1[0] == '' && $arg2[0] == '0')) {
                        $result = array(true, $token[1], 'NUMBER');
                    } elseif (!$bBothNumeric && $bBothString) {
                        if ($isForcedString) {
                            $this->RDP_AddWarning(new EMWarningInvalidComparison($token));
                        }
                        $result = array(strcmp((string) $arg1[0], (string) $arg2[0]) <= 0, $token[1], 'NUMBER');
                    } else {
                        $result = array(($arg1[0] <= $arg2[0]), $token[1], 'NUMBER');
                    }
                }
                break;
            case '>':
            case 'gt':
                if ($bMismatchType) {
                    if ($isForcedString) {
                        $this->RDP_AddWarning(new EMWarningInvalidComparison($token));
                    }
                    $result = array(false, $token[1], 'NUMBER');
                } else {
                    // Need this explicit comparison in order to be in agreement with JavaScript : still needed since we use ==='' ?
                    if (($arg1[0] == '0' && $arg2[0] == '') || ($arg1[0] == '' && $arg2[0] == '0')) {
                        $result = array(false, $token[1], 'NUMBER');
                    } elseif (!$bBothNumeric && $bBothString) {
                        if ($isForcedString) {
                            $this->RDP_AddWarning(new EMWarningInvalidComparison($token));
                        }
                        $result = array(strcmp((string) $arg1[0], (string) $arg2[0]) > 0, $token[1], 'NUMBER');
                    } else {
                        $result = array(($arg1[0] > $arg2[0]), $token[1], 'NUMBER');
                    }
                }
                break;
            case '>=';
            case 'ge':
                if ($bMismatchType) {
                    if ($isForcedString) {
                        $this->RDP_AddWarning(new EMWarningInvalidComparison($token));
                    }
                    $result = array(false, $token[1], 'NUMBER');
                } elseif (!$bBothNumeric && $bBothString) {
                    if ($isForcedString) {
                        $this->RDP_AddWarning(new EMWarningInvalidComparison($token));
                    }
                    $result = array(strcmp((string) $arg1[0], (string) $arg2[0]) >= 0, $token[1], 'NUMBER');
                } else {
                    $result = array(($arg1[0] >= $arg2[0]), $token[1], 'NUMBER');
                }
                break;
            case '+':
                if ($bBothNumeric) {
                    $this->RDP_AddWarning(new EMWarningPlusOperator($token));
                    $result = array(($arg1[0] + $arg2[0]), $token[1], 'NUMBER');
                } else {
                    $this->RDP_AddWarning(new EMWarningPlusOperator($token));
                    $result = array($arg1[0] . $arg2[0], $token[1], 'STRING');
                }
                break;
            case '-':
                if ($bBothNumeric) {
                    $result = array(($arg1[0] - $arg2[0]), $token[1], 'NUMBER');
                } else {
                    $result = array(NAN, $token[1], 'NUMBER');
                }
                break;
            case '*':
                if ($bBothNumeric) {
                    $result = array((doubleVal($arg1[0]) * doubleVal($arg2[0])), $token[1], 'NUMBER');
                } else {
                    $result = array(NAN, $token[1], 'NUMBER');
                }
                break;
            case '/';
                if ($bBothNumeric) {
                    if ($arg2[0] == 0) {
                        $result = array(NAN, $token[1], 'NUMBER');
                    } else {
                        $result = array(($arg1[0] / $arg2[0]), $token[1], 'NUMBER');
                    }
                } else {
                    $result = array(NAN, $token[1], 'NUMBER');
                }
                break;
        }

        $this->RDP_StackPush($result);
        return true;
    }

    /**
     * Processes operations like +a, -b, !c
     * @param array $token
     * @return boolean - true if success, false if any error occurred
     */

    private function RDP_EvaluateUnary(array $token)
    {
        if (count($this->RDP_stack) < 1) {
            $this->RDP_AddError(self::gT("Unable to evaluate unary operator - no entries on stack"), $token);
            return false;
        }
        $arg1 = $this->RDP_StackPop();
        if (is_null($arg1)) {
            $this->RDP_AddError(self::gT("Invalid value(s) on the stack"), $token);
            return false;
        }
        // If argmument is empty, then assume it is 0
        if ($arg1[0] == '') {
             $arg1[0] = 0;
        };
        // TODO:  try to determine datatype?
        switch ($token[0]) {
            case '+':
                $result = array((+$arg1[0]), $token[1], 'NUMBER');
                break;
            case '-':
                $result = array((-$arg1[0]), $token[1], 'NUMBER');
                break;
            case '!';
                $result = array((!$arg1[0]), $token[1], 'NUMBER');
                break;
        }
        $this->RDP_StackPush($result);
        return true;
    }


    /**
     * Main entry function
     * @param string $expr
     * @param boolean $onlyparse - if true, then validate the syntax without computing an answer
     * @param boolean $resetErrorsAndWarnings - if true (default), EM errors and warnings will be cleared before evaluation
     * @return boolean - true if success, false if any error occurred
     */
    public function RDP_Evaluate($expr, $onlyparse = false, $resetErrorsAndWarnings = true)
    {
        $this->RDP_expr = $expr;
        $this->RDP_tokens = $this->RDP_Tokenize($expr);
        $this->RDP_count = count($this->RDP_tokens);
        $this->RDP_pos = -1; // starting position within array (first act will be to increment it)
        if ($resetErrorsAndWarnings) {
            $this->RDP_errs = array();
            $this->RDP_warnings = array();
        }
        $this->RDP_onlyparse = $onlyparse;
        $this->RDP_stack = array();
        $this->RDP_evalStatus = false;
        $this->RDP_result = null;
        $this->varsUsed = array();
        $this->jsExpression = null;

        if ($this->HasSyntaxErrors()) {
            return false;
        } elseif ($this->RDP_EvaluateExpressions()) {
            if ($this->RDP_pos < $this->RDP_count) {
                $this->RDP_AddError(self::gT("Extra tokens found"), $this->RDP_tokens[$this->RDP_pos]);
                return false;
            }
            $this->RDP_result = $this->RDP_StackPop();
            if (is_null($this->RDP_result)) {
                return false;
            }
            if (count($this->RDP_stack) == 0) {
                $this->RDP_evalStatus = true;
                return true;
            } else {
                $this->RDP_AddError(self::gT("Unbalanced equation - values left on stack"), null);
                return false;
            }
        } else {
            $this->RDP_AddError(self::gT("Not a valid expression"), null);
            return false;
        }
    }


    /**
     * Process "a op b" where op in (+,-,concatenate)
     * @return boolean - true if success, false if any error occurred
     */
    private function RDP_EvaluateAdditiveExpression()
    {
        if (!$this->RDP_EvaluateMultiplicativeExpression()) {
            return false;
        }
        while (($this->RDP_pos + 1) < $this->RDP_count) {
            $token = $this->RDP_tokens[++$this->RDP_pos];
            if ($token[2] == 'BINARYOP') {
                switch ($token[0]) {
                    case '+':
                    case '-';
                        if ($this->RDP_EvaluateMultiplicativeExpression()) {
                            if (!$this->RDP_EvaluateBinary($token)) {
                                return false;
                            }
                            // else continue;
                        } else {
                            return false;
                        }
                        break;
                    default:
                        --$this->RDP_pos;
                        return true;
                }
            } else {
                --$this->RDP_pos;
                return true;
            }
        }
        return true;
    }

    /**
     * Process a Constant (number of string), retrieve the value of a known variable, or process a function, returning result on the stack.
     * @return boolean|null - true if success, false if any error occurred
     */

    private function RDP_EvaluateConstantVarOrFunction()
    {
        if ($this->RDP_pos + 1 >= $this->RDP_count) {
                $this->RDP_AddError(self::gT("Poorly terminated expression - expected a constant or variable"), null);
                return false;
        }
        $token = $this->RDP_tokens[++$this->RDP_pos];
        switch ($token[2]) {
            case 'NUMBER':
            case 'DQ_STRING':
            case 'SQ_STRING':
                $this->RDP_StackPush($token);
                return true;
                // NB: No break needed
            case 'WORD':
            case 'SGQA':
                if (($this->RDP_pos + 1) < $this->RDP_count and $this->RDP_tokens[($this->RDP_pos + 1)][2] == 'LP') {
                    return $this->RDP_EvaluateFunction();
                } else {
                    if ($this->RDP_isValidVariable($token[0])) {
                        $this->varsUsed[] = $token[0]; // add this variable to list of those used in this equation
                        if (preg_match("/\.(" . $this->getRegexpStaticValidAttributes() . ")$/", (string) $token[0])) {
                            $relStatus = 1; // static, so always relevant
                        } else {
                            $relStatus = $this->GetVarAttribute($token[0], 'relevanceStatus', 1);
                        }
                        if ($relStatus == 1) {
                            $argtype = ($this->GetVarAttribute($token[0], 'onlynum', 0)) ? "NUMBER" : "WORD";
                            $result = array($this->GetVarAttribute($token[0], null, ''), $token[1], $argtype);
                        } else {
                            $result = array(null, $token[1], 'NUMBER'); // was 0 instead of NULL
                        }
                        $this->RDP_StackPush($result);
                        return true;
                    } else {
                        $this->RDP_AddError(self::gT("Undefined variable"), $token);
                        return false;
                    }
                }
                // NB: No break needed
            case 'COMMA':
                --$this->RDP_pos;
                $this->RDP_AddError("Should never get to this line?", $token);
                return false;
                // NB: No break needed
            default:
                return false;
                // NB: No break needed
        }
    }

    /**
     * Process "a == b", "a eq b", "a != b", "a ne b"
     * @return boolean - true if success, false if any error occurred
     */
    private function RDP_EvaluateEqualityExpression()
    {
        if (!$this->RDP_EvaluateRelationExpression()) {
            return false;
        }
        while (($this->RDP_pos + 1) < $this->RDP_count) {
            $token = $this->RDP_tokens[++$this->RDP_pos];
            switch (strtolower((string) $token[0])) {
                case '==':
                case 'eq':
                case '!=':
                case 'ne':
                    if ($this->RDP_EvaluateRelationExpression()) {
                        if (!$this->RDP_EvaluateBinary($token)) {
                            return false;
                        }
                        // else continue;
                    } else {
                        return false;
                    }
                    break;
                default:
                    --$this->RDP_pos;
                    return true;
            }
        }
        return true;
    }

    /**
     * Process a single expression (e.g. without commas)
     * @return boolean - true if success, false if any error occurred
     */

    private function RDP_EvaluateExpression()
    {
        if ($this->RDP_pos + 2 < $this->RDP_count) {
            $token1 = $this->RDP_tokens[++$this->RDP_pos];
            $token2 = $this->RDP_tokens[++$this->RDP_pos];
            if ($token2[2] == 'ASSIGN') {
                if ($this->RDP_isValidVariable($token1[0])) {
                    $this->varsUsed[] = $token1[0]; // add this variable to list of those used in this equation
                    if ($this->RDP_isWritableVariable($token1[0])) {
                        $evalStatus = $this->RDP_EvaluateLogicalOrExpression();
                        if ($evalStatus) {
                            $result = $this->RDP_StackPop();
                            if (!is_null($result)) {
                                $newResult = $token2;
                                $newResult[2] = 'NUMBER';
                                $newResult[0] = $this->RDP_SetVariableValue($token2[0], $token1[0], $result[0]);
                                $this->RDP_StackPush($newResult);
                            } else {
                                $evalStatus = false;
                            }
                        }
                        $this->RDP_AddWarning(new EMWarningAssignment($token2));
                        return $evalStatus;
                    } else {
                        $this->RDP_AddError(self::gT('The value of this variable can not be changed'), $token1);
                        return false;
                    }
                } else {
                    $this->RDP_AddError(self::gT('Only variables can be assigned values'), $token1);
                    return false;
                }
            } else {
                // not an assignment expression, so try something else
                $this->RDP_pos -= 2;
                return $this->RDP_EvaluateLogicalOrExpression();
            }
        } else {
            return $this->RDP_EvaluateLogicalOrExpression();
        }
    }

    /**
     * Process "expression [, expression]*
     * @return boolean - true if success, false if any error occurred
     */

    private function RDP_EvaluateExpressions()
    {
        $evalStatus = $this->RDP_EvaluateExpression();
        if (!$evalStatus) {
            return false;
        }
        while (++$this->RDP_pos < $this->RDP_count) {
            $token = $this->RDP_tokens[$this->RDP_pos];
            if ($token[2] == 'RP') {
                return true; // presumbably the end of an expression
            } elseif ($token[2] == 'COMMA') {
                if ($this->RDP_EvaluateExpression()) {
                    $secondResult = $this->RDP_StackPop();
                    $firstResult = $this->RDP_StackPop();
                    if (is_null($firstResult)) {
                        return false;
                    }
                    $this->RDP_StackPush($secondResult);
                    $evalStatus = true;
                } else {
                    return false; // an error must have occurred
                }
            } else {
                $this->RDP_AddError(self::gT("Expected expressions separated by commas"), $token);
                $evalStatus = false;
                break;
            }
        }
        while (++$this->RDP_pos < $this->RDP_count) {
            $token = $this->RDP_tokens[$this->RDP_pos];
            $this->RDP_AddError(self::gT("Extra token found after expressions"), $token);
            $evalStatus = false;
        }
        return $evalStatus;
    }

    /**
     * Process a function call
     * @return boolean|null - true if success, false if any error occurred
     */
    private function RDP_EvaluateFunction()
    {
        $funcNameToken = $this->RDP_tokens[$this->RDP_pos]; // note that don't need to increment position for functions
        $funcName = $funcNameToken[0];
        if (!$this->RDP_isValidFunction($funcName)) {
            $this->RDP_AddError(self::gT("Undefined function"), $funcNameToken);
            return false;
        }
        $token2 = $this->RDP_tokens[++$this->RDP_pos];
        if ($token2[2] != 'LP') {
            $this->RDP_AddError(self::gT("Expected left parentheses after function name"), $funcNameToken);
        }
        $params = array(); // will just store array of values, not tokens
        while ($this->RDP_pos + 1 < $this->RDP_count) {
            $token3 = $this->RDP_tokens[$this->RDP_pos + 1];
            if (count($params) > 0) {
                // should have COMMA or RP
                if ($token3[2] == 'COMMA') {
                    ++$this->RDP_pos; // consume the token so can process next clause
                    if ($this->RDP_EvaluateExpression()) {
                        $value = $this->RDP_StackPop();
                        if (is_null($value)) {
                            return false;
                        }
                        $params[] = $value[0];
                        continue;
                    } else {
                        $this->RDP_AddError(self::gT("Extra comma found in function"), $token3);
                        return false;
                    }
                }
            }
            if ($token3[2] == 'RP') {
                ++$this->RDP_pos; // consume the token so can process next clause
                return $this->RDP_RunFunction($funcNameToken, $params);
            } else {
                if ($this->RDP_EvaluateExpression()) {
                    $value = $this->RDP_StackPop();
                    if (is_null($value)) {
                        return false;
                    }
                    $params[] = $value[0];
                    continue;
                } else {
                    return false;
                }
            }
        }
    }

    /**
     * Process "a && b" or "a and b"
     * @return boolean - true if success, false if any error occurred
     */

    private function RDP_EvaluateLogicalAndExpression()
    {
        if (!$this->RDP_EvaluateEqualityExpression()) {
            return false;
        }
        while (($this->RDP_pos + 1) < $this->RDP_count) {
            $token = $this->RDP_tokens[++$this->RDP_pos];
            switch (strtolower((string) $token[0])) {
                case '&&':
                case 'and':
                    if ($this->RDP_EvaluateEqualityExpression()) {
                        if (!$this->RDP_EvaluateBinary($token)) {
                            return false;
                        }
                        // else continue
                    } else {
                        return false; // an error must have occurred
                    }
                    break;
                default:
                    --$this->RDP_pos;
                    return true;
            }
        }
        return true;
    }

    /**
     * Process "a || b" or "a or b"
     * @return boolean - true if success, false if any error occurred
     */
    private function RDP_EvaluateLogicalOrExpression()
    {
        if (!$this->RDP_EvaluateLogicalAndExpression()) {
            return false;
        }
        while (($this->RDP_pos + 1) < $this->RDP_count) {
            $token = $this->RDP_tokens[++$this->RDP_pos];
            switch (strtolower((string) $token[0])) {
                case '||':
                case 'or':
                    if ($this->RDP_EvaluateLogicalAndExpression()) {
                        if (!$this->RDP_EvaluateBinary($token)) {
                            return false;
                        }
                        // else  continue
                    } else {
                        // an error must have occurred
                        return false;
                    }
                    break;
                default:
                    // no more expressions being  ORed together, so continue parsing
                    --$this->RDP_pos;
                    return true;
            }
        }
        // no more tokens to parse
        return true;
    }

    /**
     * Process "a op b" where op in (*,/)
     * @return boolean - true if success, false if any error occurred
     */

    private function RDP_EvaluateMultiplicativeExpression()
    {
        if (!$this->RDP_EvaluateUnaryExpression()) {
            return  false;
        }
        while (($this->RDP_pos + 1) < $this->RDP_count) {
            $token = $this->RDP_tokens[++$this->RDP_pos];
            if ($token[2] == 'BINARYOP') {
                switch ($token[0]) {
                    case '*':
                    case '/';
                        if ($this->RDP_EvaluateUnaryExpression()) {
                            if (!$this->RDP_EvaluateBinary($token)) {
                                return false;
                            }
                            // else  continue
                        } else {
                            // an error must have occurred
                            return false;
                        }
                        break;
                    default:
                        --$this->RDP_pos;
                        return true;
                }
            } else {
                --$this->RDP_pos;
                return true;
            }
        }
        return true;
    }

    /**
     * Process expressions including functions and parenthesized blocks
     * @return boolean|null - true if success, false if any error occurred
     */

    private function RDP_EvaluatePrimaryExpression()
    {
        if (($this->RDP_pos + 1) >= $this->RDP_count) {
            $this->RDP_AddError(self::gT("Poorly terminated expression - expected a constant or variable"), null);
            return false;
        }
        $token = $this->RDP_tokens[++$this->RDP_pos];
        if ($token[2] == 'LP') {
            if (!$this->RDP_EvaluateExpressions()) {
                return false;
            }
            $token = $this->RDP_tokens[$this->RDP_pos];
            if ($token[2] == 'RP') {
                return true;
            } else {
                $this->RDP_AddError(self::gT("Expected right parentheses"), $token);
                return false;
            }
        } else {
            --$this->RDP_pos;
            return $this->RDP_EvaluateConstantVarOrFunction();
        }
    }

    /**
     * Process "a op b" where op in (lt, gt, le, ge, <, >, <=, >=)
     * @return boolean - true if success, false if any error occurred
     */
    private function RDP_EvaluateRelationExpression()
    {
        if (!$this->RDP_EvaluateAdditiveExpression()) {
            return false;
        }
        while (($this->RDP_pos + 1) < $this->RDP_count) {
            $token = $this->RDP_tokens[++$this->RDP_pos];
            switch (strtolower((string) $token[0])) {
                case '<':
                case 'lt':
                case '<=';
                case 'le':
                case '>':
                case 'gt':
                case '>=';
                case 'ge':
                    if ($this->RDP_EvaluateAdditiveExpression()) {
                        if (!$this->RDP_EvaluateBinary($token)) {
                            return false;
                        }
                        // else  continue
                    } else {
                        // an error must have occurred
                        return false;
                    }
                    break;
                default:
                    --$this->RDP_pos;
                    return true;
            }
        }
        return true;
    }

    /**
     * Process "op a" where op in (+,-,!)
     * @return boolean|null - true if success, false if any error occurred
     */

    private function RDP_EvaluateUnaryExpression()
    {
        if (($this->RDP_pos + 1) >= $this->RDP_count) {
            $this->RDP_AddError(self::gT("Poorly terminated expression - expected a constant or variable"), null);
            return false;
        }
        $token = $this->RDP_tokens[++$this->RDP_pos];
        if ($token[2] == 'NOT' || $token[2] == 'BINARYOP') {
            switch ($token[0]) {
                case '+':
                case '-':
                case '!':
                    if (!$this->RDP_EvaluatePrimaryExpression()) {
                        return false;
                    }
                    return $this->RDP_EvaluateUnary($token);
                    // NB: No break needed
                default:
                    --$this->RDP_pos;
                    return $this->RDP_EvaluatePrimaryExpression();
            }
        } else {
            --$this->RDP_pos;
            return $this->RDP_EvaluatePrimaryExpression();
        }
    }

    /**
     * Returns array of all JavaScript-equivalent variable names used when parsing a string via sProcessStringContainingExpressions
     * @return array
     */
    public function GetAllJsVarsUsed()
    {
        if (is_null($this->allVarsUsed)) {
            return array();
        }
        $names = array_unique($this->allVarsUsed);
        if (is_null($names)) {
            return array();
        }
        $jsNames = array();
        foreach ($names as $name) {
            if (preg_match("/\.(" . $this->getRegexpStaticValidAttributes() . ")$/", (string) $name)) {
                continue;
            }
            $val = $this->GetVarAttribute($name, 'jsName', '');
            if ($val != '') {
                $jsNames[] = $val;
            }
        }
        return array_unique($jsNames);
    }

    /**
     * Return the list of all of the JavaScript variables used by the most recent expression - only those that are set on the current page
     * This is used to control static vs dynamic substitution.  If an expression is entirely made up of off-page changes, it can be statically replaced.
     * @return array
     */
    public function GetOnPageJsVarsUsed()
    {
        if (is_null($this->varsUsed)) {
            return array();
        }
        if ($this->surveyMode == 'survey') {
            return $this->GetJsVarsUsed();
        }
        $names = array_unique($this->varsUsed);
        if (is_null($names)) {
            return array();
        }
        $jsNames = array();
        foreach ($names as $name) {
            if (preg_match("/\.(" . $this->getRegexpStaticValidAttributes() . ")$/", (string) $name)) {
                continue;
            }
            $val = $this->GetVarAttribute($name, 'jsName', '');
            switch ($this->surveyMode) {
                case 'group':
                    $gseq = $this->GetVarAttribute($name, 'gseq', '');
                    $onpage = ($gseq == $this->groupSeq);
                    break;
                case 'question':
                    $qseq = $this->GetVarAttribute($name, 'qseq', '');
                    $onpage = ($qseq == $this->questionSeq);
                    break;
                case 'survey':
                    $onpage = true;
                    break;
            }
            if ($val != '' && $onpage) {
                $jsNames[] = $val;
            }
        }
        return array_unique($jsNames);
    }

    /**
     * Return the list of all of the JavaScript variables used by the most recent expression
     * @return array
     */
    public function GetJsVarsUsed()
    {
        if (is_null($this->varsUsed)) {
            return array();
        }
        $names = array_unique($this->varsUsed);
        if (is_null($names)) {
            return array();
        }
        $jsNames = array();
        foreach ($names as $name) {
            if (preg_match("/\.(" . $this->getRegexpStaticValidAttributes() . ")$/", (string) $name)) {
                continue;
            }
            $val = $this->GetVarAttribute($name, 'jsName', '');
            if ($val != '') {
                $jsNames[] = $val;
            }
        }
        return array_unique($jsNames);
    }

    /**
     * @return void
     */
    public function SetJsVarsUsed($vars)
    {
        $this->varsUsed = $vars;
    }

    /**
     * Return the JavaScript variable name for a named variable
     * @param string $name
     * @return string
     */
    public function GetJsVarFor($name)
    {
        return $this->GetVarAttribute($name, 'jsName', '');
    }

    /**
     * Returns array of all variables used when parsing a string via sProcessStringContainingExpressions
     * @return array
     */
    public function GetAllVarsUsed()
    {
        return array_unique($this->allVarsUsed);
    }

    /**
     * Return the result of evaluating the equation - NULL if  error
     * @return mixed
     */
    public function GetResult()
    {
        return $this->RDP_result[0];
    }

    /**
     * Return an array of errors
     * @return array
     */
    public function GetErrors()
    {
        return $this->RDP_errs;
    }

    /**
     * Converts the most recent expression into a valid JavaScript expression, mapping function and variable names and operators as needed.
     * @return string the JavaScript expresssion
     */
    public function GetJavaScriptEquivalentOfExpression()
    {
        if (!is_null($this->jsExpression)) {
            return $this->jsExpression;
        }
        if ($this->HasErrors()) {
            $this->jsExpression = '';
            return '';
        }
        $tokens = $this->RDP_tokens;
        /* @var string|null used for ASSIGN expression */
        $idToSet = null;
        /* @var string[] the final expression line by line (to be join at end) */
        $stringParts = array();
        $numTokens = count($tokens);

        /* @var integer bracket count for static function management */
        $bracket = 0;
        /* @var string static string to be parsed bedfore send to JS */
        $staticStringToParse = "";
        for ($i = 0; $i < $numTokens; ++$i) {
            $token = $tokens[$i]; // When do these need to be quoted?
            if (!empty($staticStringToParse)) { /* Currently inside a static function */
                switch ($token[2]) {
                    case 'LP':
                        $staticStringToParse .= $token[0];
                        $bracket++;
                        break;
                    case 'RP':
                        $staticStringToParse .= $token[0];
                        $bracket--;
                        break;
                    case 'DQ_STRING':
                        // A string inside double quote : add double quote again
                        $staticStringToParse .= '"' . $token[0] . '"';
                        break;
                    case 'SQ_STRING':
                        // A string inside single quote : add single quote again
                        $staticStringToParse .= "'" . $token[0] . "'";
                        break;
                    default:
                        // This set whole string inside function as a static var : must document clearly.
                        $staticStringToParse .= $token[0];
                }
                if ($bracket == 0) { // Last close bracket : get the static final function and reset
                    //~ $staticString = LimeExpressionManager::ProcessStepString("{".$staticStringToParse."}",array(),3,true);
                    $staticString = $this->sProcessStringContainingExpressions("{" . $staticStringToParse . "}", 0, 3, 1, -1, -1, true); // As static : no gseq,qseq etc …
                    $stringParts[] = "'" . addcslashes($staticString, "'") . "'";
                    $staticStringToParse = "";
                }
            } else {
                switch ($token[2]) {
                    case 'DQ_STRING':
                        $stringParts[] = '"' . addcslashes((string) $token[0], '\"') . '"'; // htmlspecialchars($token[0],ENT_QUOTES,'UTF-8',false) . "'";
                        break;
                    case 'SQ_STRING':
                        $stringParts[] = "'" . addcslashes((string) $token[0], "\'") . "'"; // htmlspecialchars($token[0],ENT_QUOTES,'UTF-8',false) . "'";
                        break;
                    case 'SGQA':
                    case 'WORD':
                        if ($i + 1 < $numTokens && $tokens[$i + 1][2] == 'LP') {
                            // then word is a function name
                            $funcInfo = $this->RDP_ValidFunctions[$token[0]];
                            if ($funcInfo[1] === null) {
                                /* start a static function */
                                $staticStringToParse = $token[0]; // The function name
                                $bracket = 0; // Reset bracket (again)
                            } else {
                                $stringParts[] = $funcInfo[1]; // the PHP function name
                            }
                        } elseif ($i + 1 < $numTokens && $tokens[$i + 1][2] == 'ASSIGN') {
                            $jsName = $this->GetVarAttribute($token[0], 'jsName', '');
                            /* Value is in the page : can not set */
                            if (!empty($jsName)) {
                                $idToSet = $jsName;
                                if ($tokens[$i + 1][0] == '+=') {
                                    // Javascript does concatenation unless both left and right side are numbers, so refactor the equation
                                    $varName = $this->GetVarAttribute($token[0], 'varName', $token[0]);
                                    $stringParts[] = " = LEMval('" . $varName . "') + ";
                                    ++$i;
                                }
                            }
                        } else {
                            if (preg_match("/\.(" . $this->getRegexpStaticValidAttributes() . ")$/", (string) $token[0])) {
                                /* This is a static variables : set as static */
                                $static = $this->sProcessStringContainingExpressions("{" . $token[0] . "}", 0, 1, 1, -1, -1, true);
                                $stringParts[] = "'" . addcslashes($static, "'") . "'";
                            } else {
                                $jsName = $this->GetVarAttribute($token[0], 'jsName', '');
                                $code = $this->GetVarAttribute($token[0], 'code', '');
                                if ($jsName != '') {
                                    $varName = $this->GetVarAttribute($token[0], 'varName', $token[0]);
                                    $stringParts[] = "LEMval('" . $varName . "') ";
                                } else {
                                    $stringParts[] = "'" . addcslashes($code, "'") . "'";
                                }
                            }
                        }
                        break;
                    case 'LP':
                    case 'RP':
                        $stringParts[] = $token[0];
                        break;
                    case 'NUMBER':
                        $stringParts[] = is_numeric($token[0]) ? $token[0] : ("'" . $token[0] . "'");
                        break;
                    case 'COMMA':
                        $stringParts[] = $token[0] . ' ';
                        break;
                    default:
                        // don't need to check type of $token[2] here since already handling SQ_STRING and DQ_STRING above
                        switch (strtolower((string) $token[0])) {
                            case 'and':
                                $stringParts[] = ' && ';
                                break;
                            case 'or':
                                $stringParts[] = ' || ';
                                break;
                            case 'lt':
                                $stringParts[] = ' < ';
                                break;
                            case 'le':
                                $stringParts[] = ' <= ';
                                break;
                            case 'gt':
                                $stringParts[] = ' > ';
                                break;
                            case 'ge':
                                $stringParts[] = ' >= ';
                                break;
                            case 'eq':
                            case '==':
                                $stringParts[] = ' == ';
                                break;
                            case 'ne':
                            case '!=':
                                $stringParts[] = ' != ';
                                break;
                            case '=':
                                /* ASSIGN : usage jquery: don't add anything (disable default) */;
                                break;
                            default:
                                $stringParts[] = ' ' . $token[0] . ' ';
                                break;
                        }
                        break;
                }
            }
        }
        // for each variable that does not have a default value, add clause to throw error if any of them are NA
        $nonNAvarsUsed = array();
        foreach ($this->GetVarsUsed() as $var) {
            /* This function wants to see the NAOK suffix (NAOK|valueNAOK|shown)
             * OR static var and Check dynamic var inside static function too
             * see https://bugs.limesurvey.org/view.php?id=18008 for issue about sgqa and question
             * See https://bugs.limesurvey.org/view.php?id=14818 for feature
             */
            if (!preg_match("/^.*\.(NAOK|valueNAOK|shown|relevanceStatus)$/", (string) $var) &&  !preg_match("/^.*\.(" . $this->getRegexpStaticValidAttributes() . ")$/", (string) $var)) {
                if ($this->GetVarAttribute($var, 'jsName', '') != '') {
                    $nonNAvarsUsed[] = $var;
                }
            }
        }
        $mainClause = implode('', $stringParts);
        if ($idToSet) {
            /* If there are an id to set (assign) : set it via jquery */
            $mainClause = "$('#{$idToSet}').val({$mainClause})";
        }
        $varsUsed = implode("', '", $nonNAvarsUsed);
        if ($varsUsed != '') {
            $this->jsExpression = "LEMif(LEManyNA('" . $varsUsed . "'),'',(" . $mainClause . "))";
        } else {
            $this->jsExpression = '(' . $mainClause . ')';
        }
        return $this->jsExpression;
    }

    /**
     * JavaScript Test function - simply writes the result of the current JavaScriptEquivalentFunction to the output buffer.
     * @param string $expected
     * @param integer $num
     * @return string
     */
    public function GetJavascriptTestforExpression($expected, $num)
    {
        // assumes that the hidden variables have already been declared
        $expr = $this->GetJavaScriptEquivalentOfExpression();
        if (is_null($expr) || $expr == '') {
            $expr = "'NULL'";
        }
        $jsmultiline_expr = str_replace("\n", "\\\n", $expr);
        $jsmultiline_expected = str_replace("\n", "\\\n", addslashes($expected));
        $jsParts = array();
        $jsParts[] = "val = " . $jsmultiline_expr . ";\n";
        $jsParts[] = "klass = (LEMeq(addslashes(val),'" . $jsmultiline_expected . "')) ? 'ok' : 'error';\n";
        $jsParts[] = "document.getElementById('test_" . $num . "').innerHTML=(val);\n";
        $jsParts[] = "document.getElementById('test_" . $num . "').className=klass;\n";
        return implode('', $jsParts);
    }

    /**
     * Generate the function needed to dynamically change the value of a <span> section
     * @param integer $questionNum No longer used
     * @param string $elementId - the ID name for the function
     * @param string $eqn No longer used
     * @return string : javascript part
     */
    public function GetJavaScriptFunctionForReplacement($questionNum, $elementId, $eqn)
    {
        $jsParts = array();
        $jsParts[] = "jQuery('#{$elementId}').html(LEMfixnum(\n";
        $jsParts[] = $this->GetJavaScriptEquivalentOfExpression();
        $jsParts[] = "));\n";
        // Add an event after html is updated (see #11937 and really good helper for template manager)
        $jsParts[] = "jQuery('#{$elementId}').trigger('html:updated');\n"; // See http://learn.jquery.com/events/introduction-to-custom-events/#naming-custom-events for colons in name
        return implode('', $jsParts);
    }

    /**
     * Returns the most recent PrettyPrint string generated by sProcessStringContainingExpressions
     */
    public function GetLastPrettyPrintExpression()
    {
        return $this->prettyPrintSource;
    }

    /**
     * This is only used when there are no needed substitutions
     * @param string $expr
     */
    public function SetPrettyPrintSource($expr)
    {
        $this->prettyPrintSource = $expr;
    }

    /**
     * Color-codes Expressions (using HTML <span> tags), showing variable types and values.
     * @return string HTML
     */
    public function GetPrettyPrintString()
    {
        //~ Yii::app()->setLanguage(Yii::app()->session['adminlang']);
        // color code the equation, showing not only errors, but also variable attributes
        $errs = $this->RDP_errs;
        $tokens = $this->RDP_tokens;
        $errCount = count($errs);
        $errIndex = 0;
        if ($errCount > 0) {
            usort($errs, "cmpErrorTokens");
        }
        $warnings = $this->RDP_warnings;
        $warningsCount = count($warnings);
        if (!empty($warnings)) {
            usort($warnings, "cmpWarningTokens");
        }
        $stringParts = array();
        $numTokens = count($tokens);
        $bHaveError = false;

        $globalErrs = array(); // Error not related to a token (bracket for example)
        while ($errIndex < $errCount) {
            if (empty($errs[$errIndex][1])) {
                $globalErrs[] = $errs[$errIndex][0];
                $bHaveError = true;
            }
            $errIndex++;
        }

        for ($i = 0; $i < $numTokens; ++$i) {
            $token = $tokens[$i];
            $messages = array();
            $thisTokenHasError = false;
            $errIndex = 0;
            while ($errIndex < $errCount) {
                if ($errs[$errIndex][1] == $token) { // Error related to this token
                    $messages[] = $errs[$errIndex][0];
                    $thisTokenHasError = true;
                }
                $errIndex++;
            }
            $thisTokenHasWarning = false;
            $warningIndex = 0;
            while ($warningIndex < $warningsCount) {
                if ($warnings[$warningIndex]->getToken() == $token) { // Error related to this token
                    $messages[] = $warnings[$warningIndex]->getMessage();
                    $thisTokenHasWarning = true;
                }
                $warningIndex++;
            }
            if ($thisTokenHasError) {
                $stringParts[] = "<span class='em-error' title=' ' >";
                $bHaveError = true;
            } elseif ($thisTokenHasWarning) {
                $stringParts[] = "<span class='em-warning' title=' '>";
            }
            switch ($token[2]) {
                case 'DQ_STRING':
                    /* Check $token[0] forced string */
                    $stringParts[] = CHtml::tag('span', array(
                        'title' => !empty($messages) ? implode('; ', $messages) : null,
                        'class' => 'em-var-string'
                    ), "\"" . $token[0] . "\"");
                    break;
                case 'SQ_STRING':
                    $stringParts[] = CHtml::tag('span', array(
                        'title' => !empty($messages) ? implode('; ', $messages) : null,
                        'class' => 'em-var-string'
                    ), "'" . CHtml::encode($token[0]) . "'");
                    break;
                case 'SGQA':
                case 'WORD':
                    if ($i + 1 < $numTokens && $tokens[$i + 1][2] == 'LP') {
                        // then word is a function name
                        if ($this->RDP_isValidFunction($token[0])) {
                            $funcInfo = $this->RDP_ValidFunctions[$token[0]];
                            $messages[] = $funcInfo[2];
                            $messages[] = $funcInfo[3];
                        }
                        $stringParts[] = "<span title='" . CHtml::encode(implode('; ', $messages)) . "' class='em-function' >";
                        $stringParts[] = $token[0];
                        $stringParts[] = "</span>";
                    } else {
                        if (!$this->RDP_isValidVariable($token[0])) {
                            $class = 'em-var-error';
                            $displayName = $token[0];
                        } else {
                            $jsName = $this->GetVarAttribute($token[0], 'jsName', '');
                            $code = $this->GetVarAttribute($token[0], 'code', '');
                            $question = $this->GetVarAttribute($token[0], 'question', '');
                            $qcode = $this->GetVarAttribute($token[0], 'qcode', '');
                            $questionSeq = $this->GetVarAttribute($token[0], 'qseq', -1);
                            $groupSeq = $this->GetVarAttribute($token[0], 'gseq', -1);
                            $ansList = $this->GetVarAttribute($token[0], 'ansList', '');
                            $gid = $this->GetVarAttribute($token[0], 'gid', -1);
                            $qid = $this->GetVarAttribute($token[0], 'qid', -1);

                            if ($jsName != '') {
                                $descriptor = '[' . $jsName . ']';
                            } else {
                                $descriptor = '';
                            }
                            // Show variable name instead of SGQA code, if available
                            if ($qcode != '') {
                                if (preg_match('/^INSERTANS:/', (string) $token[0])) {
                                    $displayName = $qcode . '.shown';
                                    $descriptor = '[' . $token[0] . ']';
                                } else {
                                    $args = explode('.', (string) $token[0]);
                                    if (count($args) == 2) {
                                        $displayName = $qcode . '.' . $args[1];
                                    } else {
                                        $displayName = $qcode;
                                    }
                                }
                            } else {
                                $displayName = $token[0];
                            }
                            if ($questionSeq != -1) {
                                $descriptor .= '[G:' . $groupSeq . ']';
                            }
                            if ($groupSeq != -1) {
                                $descriptor .= '[Q:' . $questionSeq . ']';
                            }
                            if (strlen($descriptor) > 0) {
                                $descriptor .= ': ';
                            }

                            $messages[] = $descriptor . $question;
                            if ($ansList != '') {
                                $messages[] = $ansList;
                            }
                            if ($code != '') {
                                if ($token[2] == 'SGQA' && preg_match('/^INSERTANS:/', (string) $token[0])) {
                                    $shown = $this->GetVarAttribute($token[0], 'shown', '');
                                    $messages[] = 'value=[' . $code . '] '
                                            . $shown;
                                } else {
                                    $messages[] = 'value=' . $code;
                                }
                            }

                            if ($this->groupSeq == -1 || $groupSeq == -1 || $questionSeq == -1 || $this->questionSeq == -1) {
                                $class = 'em-var-static';
                            } elseif ($groupSeq > $this->groupSeq) {
                                $class = 'em-var-before em-var-diffgroup';
                            } elseif ($groupSeq < $this->groupSeq) {
                                $class = 'em-var-after ';
                            } elseif ($questionSeq > $this->questionSeq) {
                                $class = 'em-var-before em-var-inpage';
                            } else {
                                $class = 'em-var-after em-var-inpage';
                            }
                        }
                        // prevent EM prcessing of messages within span
                        $message = implode('; ', $messages);
                        $message = str_replace(array('{', '}'), array('{ ', ' }'), $message);

                        if ($this->hyperlinkSyntaxHighlighting && isset($gid) && isset($qid) && $qid > 0 && $this->RDP_isValidVariable($token[0])) {
                            $editlink = App()->getController()->createUrl('questionAdministration/view/surveyid/' . $this->sid . '/gid/' . $gid . '/qid/' . $qid);
                            $stringParts[] = "<a title='" . CHtml::encode($message) . "' class='em-var {$class}' href='{$editlink}' >";
                        } else {
                            $stringParts[] = "<span title='" . CHtml::encode($message) . "' class='em-var {$class}' >";
                        }
                        if ($this->sgqaNaming) {
                            $sgqa = substr((string) $jsName, 4);
                            $nameParts = explode('.', (string) $displayName);
                            if (count($nameParts) == 2) {
                                $sgqa .= '.' . $nameParts[1];
                            }
                            $stringParts[] = $sgqa;
                        } else {
                            $stringParts[] = $displayName;
                        }
                        if ($this->hyperlinkSyntaxHighlighting && isset($gid) && isset($qid) && $qid > 0 && $this->RDP_isValidVariable($token[0])) {
                            $stringParts[] = "</a>";
                        } else {
                            $stringParts[] = "</span>";
                        }
                    }
                    break;
                case 'ASSIGN':
                    $stringParts[] = CHtml::tag('span', array(
                        'title' => !empty($messages) ? implode('; ', $messages) : null,
                        'class' => 'em-assign em-warning'
                    ), ' ' . $token[0] . ' ');
                    break;
                case 'COMMA':
                    $stringParts[] = $token[0] . ' ';
                    break;
                case 'LP':
                case 'RP':
                case 'NUMBER':
                    $stringParts[] = $token[0];
                    break;
                case 'COMPARE':
                    $stringParts[] = CHtml::tag('span', array(
                        'title' => !empty($messages) ? implode('; ', $messages) : null,
                        'class' => 'em-compare'
                    ), ' ' . $token[0] . ' ');
                    break;
                default:
                    $stringParts[] = CHtml::tag('span', array(
                        'title' => !empty($messages) ? implode('; ', $messages) : null,
                    ), ' ' . $token[0] . ' ');
                    break;
            }
            if ($thisTokenHasError || $thisTokenHasWarning) {
                $stringParts[] = "</span>";
                ++$errIndex;
            }
        }
        if ($this->sid && Permission::model()->hasSurveyPermission($this->sid, 'surveycontent', 'update') && method_exists(App(), 'getClientScript')) {
            App()->getClientScript()->registerPackage('expressionscript');
        }
        $sClass = 'em-expression';
        $sClass .= ($bHaveError) ? " em-haveerror" : "";
        $title = "";
        if (!empty($globalErrs)) {
            $sClass .= " em-error";
            $title = " title='" . CHtml::encode(implode('; ', $globalErrs)) . "'";
        }
        return "<span class='$sClass' $title >" . implode('', $stringParts) . "</span>";
    }

    /**
     * Get information about the variable, including JavaScript name, read-write status, and whether set on current page.
     * @param string $name
     * @param string|null $attr
     * @param string $default
     * @return string
     */
    private function GetVarAttribute($name, $attr, $default)
    {
        return LimeExpressionManager::GetVarAttribute($name, $attr, $default, $this->groupSeq, $this->questionSeq);
    }

    /**
     * Return array of the list of variables used  in the equation
     * @return array
     */
    public function GetVarsUsed()
    {
        return array_unique($this->varsUsed);
    }

    /**
     * Return true if there were syntax or processing errors
     * @return boolean
     */
    public function HasErrors()
    {
        return (count($this->RDP_errs) > 0);
    }

    /**
     * Return array of warnings
     * @return array
     */
    public function GetWarnings()
    {
        return $this->RDP_warnings;
    }

    /**
     * Reset current warnings
     * @see Related issue #15547: Invalid error count on Survey Logic file for subquestion relevance
     * @link https://bugs.limesurvey.org/view.php?id=15547
     * ProcessBooleanExpression didn't reset RDP_errors anb RDP_warnings, need a way to reset for Survey logic checking
     * @return void
     */
    public function ResetWarnings()
    {
        $this->RDP_warnings = array();
    }

    /**
     * Reset current errors
     * @see Related issue #16738: https://bugs.limesurvey.org/view.php?id=16738
     * @link https://bugs.limesurvey.org/view.php?id=16738
     * ProcessBooleanExpression didn't reset RDP_errors anb RDP_warnings, need a way to reset for Survey logic checking
     * @return void
     */
    public function ResetErrors()
    {
        $this->RDP_errs = array();
    }

    /**
     * Reset current errors and current warnings
     * @return void
     */
    public function ResetErrorsAndWarnings()
    {
        $this->ResetErrors();
        $this->ResetWarnings();
    }

    /**
     * Return true if there are syntax errors
     * @return boolean
     */
    private function HasSyntaxErrors()
    {
        // check for bad tokens
        // check for unmatched parentheses
        // check for undefined variables
        // check for undefined functions (but can't easily check allowable # elements?)

        $nesting = 0;

        for ($i = 0; $i < $this->RDP_count; ++$i) {
            $token = $this->RDP_tokens[$i];
            switch ($token[2]) {
                case 'LP':
                    ++$nesting;
                    break;
                case 'RP':
                    --$nesting;
                    if ($nesting < 0) {
                        $this->RDP_AddError(self::gT("Extra right parentheses detected"), $token);
                    }
                    break;
                case 'WORD':
                case 'SGQA':
                    if ($i + 1 < $this->RDP_count and $this->RDP_tokens[$i + 1][2] == 'LP') {
                        if (!$this->RDP_isValidFunction($token[0])) {
                            $this->RDP_AddError(self::gT("Undefined function"), $token);
                        }
                    } else {
                        if (!($this->RDP_isValidVariable($token[0]))) {
                            $this->RDP_AddError(self::gT("Undefined variable"), $token);
                        }
                    }
                    break;
                case 'OTHER':
                    $this->RDP_AddError(self::gT("Unsupported syntax"), $token);
                    break;
                default:
                    break;
            }
        }
        if ($nesting > 0) {
            $this->RDP_AddError(sprintf(self::gT("Missing %s closing right parentheses"), $nesting), null);
        }
        if ($nesting < 0) {
            $this->RDP_AddError(sprintf(self::gT("Missing %s closing left parentheses"), abs($nesting)), null);
        }
        return (count($this->RDP_errs) > 0);
    }

    /**
     * Return true if the function name is registered
     * @param string $name
     * @return boolean
     */

    private function RDP_isValidFunction($name)
    {
        return array_key_exists($name, $this->RDP_ValidFunctions);
    }

    /**
     * Add extra attributes for var
     * @param string[] $extraAttributes
     * @param boolean $static is a static attribute , unused currently since there are no way to create the EM js system
     * @return void
     */
    public function addRegexpExtraAttributes($extraAttributes, $static = true)
    {
        if (!$static) {
            $this->aRDP_regexpVariableAttribute = array_merge($this->aRDP_regexpVariableAttribute, $extraAttributes);
        } else {
            $this->aRDP_regexpStaticAttribute = array_merge($this->aRDP_regexpStaticAttribute, $extraAttributes);
        }
    }

    public function getRegexpValidAttributes()
    {
        /* Static var or cache it ? Must control when updated */
        return implode("|", array_merge($this->aRDP_regexpVariableAttribute, $this->aRDP_regexpStaticAttribute));
    }

    public function getRegexpStaticValidAttributes()
    {
        /* Static var or cache it ? Must control when updated */
        return implode("|", $this->aRDP_regexpStaticAttribute);
    }

    /**
     * Return true if the variable name is registered
     * @param string $name
     * @return boolean
     */
    private function RDP_isValidVariable($name)
    {
        $varName = preg_replace("/^(?:INSERTANS:)?(.*?)(?:\.(?:" . $this->getRegexpValidAttributes() . "))?$/", "$1", $name);
        return LimeExpressionManager::isValidVariable($varName);
    }

    /**
     * Return true if the variable name is writable
     * @param string $name
     * @return boolean
     */
    private function RDP_isWritableVariable($name)
    {
        return ($this->GetVarAttribute($name, 'readWrite', 'N') == 'Y');
    }

    /**
     * Process an expression and return its boolean value
     * @param string $expr
     * @param int $groupSeq - needed to determine whether using variables before they are declared
     * @param int $questionSeq - needed to determine whether using variables before they are declared
     * @return boolean
     */
    public function ProcessBooleanExpression($expr, $groupSeq = -1, $questionSeq = -1)
    {
        $this->groupSeq = $groupSeq;
        $this->questionSeq = $questionSeq;

        $expr = $this->ExpandThisVar($expr);
        $status = $this->RDP_Evaluate($expr);
        if (!$status) {
            return false; // if there are errors in the expression, hide it?
        }
        $result = $this->GetResult();
        if (is_null($result)) {
            return false; // if there are errors in the expression, hide it?
        }

        foreach ($this->GetVarsUsed() as $var) {
            /* this function wants to see the NAOK suffix : NAOK|valueNAOK|shown|relevanceStatus
             * Static suffix are always OK (no need NAOK)
             */
            if (!preg_match("/^.*\.(NAOK|valueNAOK|shown|relevanceStatus)$/", (string) $var) && ! preg_match("/\.(" . $this->getRegexpStaticValidAttributes() . ")$/", (string) $var)) {
                if (!LimeExpressionManager::GetVarAttribute($var, 'relevanceStatus', false, $groupSeq, $questionSeq)) {
                    return false;
                }
            }
        }
        return (bool) $result;
    }

    /**
     * Start processing a group of substitions - will be incrementally numbered
     */

    public function StartProcessingGroup($sid = null, $rooturl = '', $hyperlinkSyntaxHighlighting = true)
    {
        $this->substitutionNum = 0;
        $this->substitutionInfo = array(); // array of JavaScripts for managing each substitution
        $this->sid = $sid;
        $this->hyperlinkSyntaxHighlighting = $hyperlinkSyntaxHighlighting;
    }

    /**
     * Clear cache of tailoring content.
     * When re-displaying same page, need to avoid generating double the amount of tailoring content.
     */
    public function ClearSubstitutionInfo()
    {
        $this->substitutionNum = 0;
        $this->substitutionInfo = array(); // array of JavaScripts for managing each substitution
    }

    /**
     * Process multiple substitution iterations of a full string, containing multiple expressions delimited by {}, return a consolidated string
     * @param string $src
     * @param int $questionNum
     * @param int $numRecursionLevels - number of levels of recursive substitution to perform
     * @param int $whichPrettyPrintIteration - if recursing, specify which pretty-print iteration is desired
     * @param int $groupSeq - needed to determine whether using variables before they are declared
     * @param int $questionSeq - needed to determine whether using variables before they are declared
     * @param boolean $staticReplacement
     * @return string
     */
    public function sProcessStringContainingExpressions($src, $questionNum = 0, $numRecursionLevels = 1, $whichPrettyPrintIteration = 1, $groupSeq = -1, $questionSeq = -1, $staticReplacement = false)
    {
        // tokenize string by the {} pattern, properly dealing with strings in quotations, and escaped curly brace values
        $this->allVarsUsed = array();
        $this->questionSeq = $questionSeq;
        $this->groupSeq = $groupSeq;
        $result = $src;
        $prettyPrint = '';

        $prettyPrintIterationDone = false;
        for ($i = 1; $i <= $numRecursionLevels; ++$i) {
            // TODO - Since want to use <span> for dynamic substitution, what if there are recursive substititons?
            $prevResult = $result;
            $result = $this->sProcessStringContainingExpressionsHelper($result, $questionNum, $staticReplacement);
            if ($result === $prevResult) {
                // No update during process : can exit of iteration
                if (!$prettyPrintIterationDone) {
                    $prettyPrint = $this->prettyPrintSource;
                }
                // No need errors : already done
                break;
            }
            if ($i == $whichPrettyPrintIteration) {
                $prettyPrint = $this->prettyPrintSource;
                $prettyPrintIterationDone = true;
            }
        }
        $this->prettyPrintSource = $prettyPrint; // ensure that if doing recursive substition, can get original source to pretty print
        $result = str_replace(array('\{', '\}',), array('{', '}'), $result);
        return $result;
    }

    /**
     * Process one substitution iteration of a full string, containing multiple expressions delimited by {}, return a consolidated string
     * @param string $src
     * @param integer $questionNum - used to generate substitution <span>s that indicate to which question they belong
     * @param boolean $staticReplacement
     * @return string
     */
    public function sProcessStringContainingExpressionsHelper($src, $questionNum, $staticReplacement = false)
    {
        // tokenize string by the {} pattern, properly dealing with strings in quotations, and escaped curly brace values
        $stringParts = $this->asSplitStringOnExpressions($src);
        $resolvedParts = array();
        $prettyPrintParts = array();
        $this->ResetErrorsAndWarnings();
        foreach ($stringParts as $stringPart) {
            if ($stringPart[2] == 'STRING') {
                $resolvedParts[] = $stringPart[0];
                $prettyPrintParts[] = $stringPart[0];
            } else {
                ++$this->substitutionNum;
                $expr = $this->ExpandThisVar(substr((string) $stringPart[0], 1, -1));
                if ($this->RDP_Evaluate($expr, false, $this->resetErrorsAndWarningsOnEachPart)) {
                    $resolvedPart = $this->GetResult();
                } else {
                    // show original and errors in-line only if user have the right to update survey content
                    if ($this->sid && Permission::model()->hasSurveyPermission($this->sid, 'surveycontent', 'update')) {
                        $resolvedPart = $this->GetPrettyPrintString();
                    } else {
                        $resolvedPart = '';
                    }
                }
                $onpageJsVarsUsed = $this->GetOnPageJsVarsUsed();
                $jsVarsUsed = $this->GetJsVarsUsed();
                $prettyPrintParts[] = $this->GetPrettyPrintString();
                $this->allVarsUsed = array_merge($this->allVarsUsed, $this->GetVarsUsed());

                if (count($onpageJsVarsUsed) > 0 && !$staticReplacement) {
                    $idName = "LEMtailor_Q_" . $questionNum . "_" . $this->substitutionNum;
                    $resolvedParts[] = "<span id='" . $idName . "'>" . $resolvedPart . "</span>";
                    $this->substitutionInfo[] = array(
                        'questionNum' => $questionNum,
                        'num' => $this->substitutionNum,
                        'id' => $idName,
                        'raw' => $stringPart[0],
                        'result' => $resolvedPart,
                        'vars' => implode('|', $jsVarsUsed),
                        'js' => $this->GetJavaScriptFunctionForReplacement($questionNum, $idName, $expr),
                    );
                } else {
                    $resolvedParts[] = $resolvedPart;
                }
            }
        }
        $result = implode('', $this->flatten_array($resolvedParts));
        $this->prettyPrintSource = implode('', $this->flatten_array($prettyPrintParts));
        return $result; // recurse in case there are nested ones, avoiding infinite loops?
    }

    /**
     * If the equation contains reference to this, expand to comma separated list if needed.
     *
     * @param string $src
     * @return string
     */
    function ExpandThisVar($src)
    {
        /** @var array */
        static $cache = [];
        if (isset($cache[$src])) {
            return $cache[$src];
        }
        /** @var boolean $setInCache set result in static $cache. mantis #14998 */
        $setInCache = true;
        /** @var string */
        $expandedVar = "";
        $tokens = $this->Tokenize($src, 1);
        foreach ($tokens as $token) {
            switch ($token[2]) {
                case 'SGQA':
                case 'WORD':
                    $splitter = '(?:\b(?:self|that))(?:\.(?:[A-Z0-9_]+))*'; // self or that, optionaly followed by dot and alnum
                    if (preg_match("/" . $splitter . "/", (string) $token[0])) {
                        $setInCache = false;
                        $expandedVar .= LimeExpressionManager::GetAllVarNamesForQ($this->questionSeq, $token[0]);
                    } else {
                        $expandedVar .= $token[0];
                    }
                    break;
                case 'DQ_STRING';
                    $expandedVar .= "\"{$token[0]}\"";
                    break;
                case 'SQ_STRING';
                    $expandedVar .= "'{$token[0]}'";
                    break;
                case 'SPACE':
                case 'LP':
                case 'RP':
                case 'COMMA':
                case 'AND_OR':
                case 'COMPARE':
                case 'NUMBER':
                case 'NOT':
                case 'OTHER':
                case 'ASSIGN':
                case 'BINARYOP':
                default:
                    $expandedVar .= $token[0];
            }
        }
        if ($setInCache) {
            $cache[$src] = $expandedVar;
        }
        return $expandedVar;
    }

    /**
     * Get info about all <span> elements needed for dynamic tailoring
     * @return array
     */
    public function GetCurrentSubstitutionInfo()
    {
        return $this->substitutionInfo;
    }

    /**
     * Flatten out an array, keeping it in the proper order
     * @param array $a
     * @return array
     */
    private function flatten_array(array $a)
    {
        $i = 0;
        while ($i < count($a)) {
            if (is_array($a[$i])) {
                array_splice($a, $i, 1, $a[$i]);
            } else {
                $i++;
            }
        }
        return $a;
    }


    /**
     * Run a registered function
     * Some PHP functions require specific data types - those can be cast here.
     * @param array $funcNameToken
     * @param array $params
     * @return boolean|null
     */
    private function RDP_RunFunction($funcNameToken, $params)
    {
        $name = $funcNameToken[0];
        if (!$this->RDP_isValidFunction($name)) {
            return false;
        }

        $func = $this->RDP_ValidFunctions[$name];
        $funcName = $func[0];
        $result = 1; // default value for $this->RDP_onlyparse
        if (is_callable($funcName)) {
            $numArgsAllowed = array_slice($func, 5); // get array of allowable argument counts from end of $func
            $argsPassed = is_array($params) ? count($params) : 0;

            // for unlimited #  parameters (any value less than 0).
            try {
                if ($numArgsAllowed[0] < 0) {
                    $minArgs = abs($numArgsAllowed[0] + 1); // so if value is -2, means that requires at least one argument
                    if ($argsPassed < $minArgs) {
                        $this->RDP_AddError(sprintf(gT("Function must have at least %s argument|Function must have at least %s arguments", $minArgs), $minArgs), $funcNameToken);
                        return false;
                    }
                    if (!$this->RDP_onlyparse) {
                        switch ($funcName) {
                            case 'sprintf':
                                /* function with any number of params */
                                $result = call_user_func_array('sprintf', $params);
                                break;
                            default:
                                /* function with array as param*/
                                $result = call_user_func($funcName, $params);
                                break;
                        }
                    }
                // Call  function with the params passed
                } elseif (in_array($argsPassed, $numArgsAllowed)) {
                    switch ($argsPassed) {
                        case 0:
                            if (!$this->RDP_onlyparse) {
                                $result = call_user_func($funcName);
                            }
                            break;
                        case 1:
                            if (!$this->RDP_onlyparse) {
                                switch ($funcName) {
                                    case 'acos':
                                    case 'asin':
                                    case 'atan':
                                    case 'cos':
                                    case 'exp':
                                    case 'is_nan':
                                    case 'sin':
                                    case 'sqrt':
                                    case 'tan':
                                    case 'ceil':
                                    case 'floor':
                                    case 'round':
                                        if (is_numeric($params[0])) {
                                            $result = $funcName(floatval($params[0]));
                                        } else {
                                            $result = NAN; // NAN in PHP …
                                        }
                                        break;
                                    default:
                                        $result = call_user_func($funcName, $params[0]);
                                        break;
                                }
                            }
                            break;
                        case 2:
                            if (!$this->RDP_onlyparse) {
                                switch ($funcName) {
                                    case 'atan2':
                                    case 'pow':
                                        if (is_numeric($params[0]) && is_numeric($params[1])) {
                                            $result = $funcName(floatval($params[0]), floatval($params[1]));
                                        } else {
                                            $result = false; // Not same than other
                                        }
                                        break;
                                    default:
                                        try {
                                            $result = call_user_func($funcName, $params[0], $params[1]);
                                        } catch (\Throwable $e) {
                                            $this->RDP_AddError($e->getMessage(), $funcNameToken);
                                            return false;
                                        }
                                        break;
                                }
                            }
                            break;
                        case 3:
                            if (!$this->RDP_onlyparse) {
                                switch ($funcName) {
                                    case 'substr':
                                        // check if params1 and 2 are integer representations
                                        if (filter_var($params[1], FILTER_VALIDATE_INT) && filter_var($params[2], FILTER_VALIDATE_INT)) {
                                            $result = $funcName(floatval($params[0]), floatval($params[1]));
                                        } else {
                                            $result = false; // Not same than other
                                        }
                                        break;
                                    default:
                                        $result = call_user_func($funcName, $params[0], $params[1], $params[2]);
                                }
                            }
                            break;
                        case 4:
                        case 5:
                        case 6:
                        default:
                            /* We can accept any fixed numbers of params with call_user_func_array */
                            if (!$this->RDP_onlyparse) {
                                $result = call_user_func_array($funcName, $params);
                            }
                            break;
                    }
                } else {
                    $this->RDP_AddError(sprintf(self::gT("Function does not support %s arguments"), $argsPassed) . ' '
                            . sprintf(self::gT("Function supports this many arguments, where -1=unlimited: %s"), implode(',', $numArgsAllowed)), $funcNameToken);
                    return false;
                }
                if (function_exists("geterrors_" . $funcName)) {
                    /* @todo allow adding it for plugin , if it work …*/
                    if ($sError = call_user_func_array("geterrors_" . $funcName, $params)) {
                        $this->RDP_AddError($sError, $funcNameToken);
                        return false;
                    }
                }
            } catch (Exception $e) {
                $this->RDP_AddError($e->getMessage(), $funcNameToken);
                return false;
            }
            $token = array($result, $funcNameToken[1], 'NUMBER');
            $this->RDP_StackPush($token);
            return true;
        }
    }

    /**
     * Add user functions to array of allowable functions within the equation.
     * $functions is an array of key to value mappings like this:
     * See $this->RDP_ValidFunctions for examples of the syntax
     * @param array $functions
     */

    public function RegisterFunctions(array $functions)
    {
        $this->RDP_ValidFunctions = array_merge($this->RDP_ValidFunctions, $functions);
    }

    /**
     * Set the value of a registered variable
     * @param string $op - the operator (=,*=,/=,+=,-=)
     * @param string $name
     * @param string $value
     * @return int
     */
    private function RDP_SetVariableValue($op, $name, $value)
    {
        if ($this->RDP_onlyparse) {
            return 1;
        }
        return LimeExpressionManager::SetVariableValue($op, $name, $value);
    }

    /**
     * Split a source string into STRING vs. EXPRESSION, where the latter is surrounded by unescaped curly braces.
     * This version properly handles nested curly braces and curly braces within strings within curly braces - both of which are needed to better support JavaScript
     * Users still need to add a space or carriage return after opening braces (and ideally before closing braces too) to avoid  having them treated as expressions.
     * @param string $src
     * @return array
     */
    public function asSplitStringOnExpressions($src)
    {
        // Empty string, return an array
        if ($src === "") {
            return array();
        }
        // No replacement to do, preg_split get more time than strpos
        if (strpos($src, "{") === false || $src === "{"  || $src === "}") {
            return array (
                0 => array ($src,0,'STRING')
            );
        };

        // Seems to need split and replacement
        $parts = preg_split($this->RDP_ExpressionRegex, $src, -1, (PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE));

        $count = count($parts);
        $tokens = array();
        $inSQString = false;
        $inDQString = false;
        $curlyDepth = 0;
        $thistoken = array();
        $offset = 0;
        for ($j = 0; $j < $count; ++$j) {
            switch ($parts[$j]) {
                case '{':
                    if ($j < ($count - 1) && preg_match('/\s|\n|\r/', substr($parts[$j + 1], 0, 1))) {
                        // don't count this as an expression if the opening brace is followed by whitespace
                        $thistoken[] = '{';
                        $thistoken[] = $parts[++$j];
                    } elseif ($inDQString || $inSQString) {
                        // just push the curly brace
                        $thistoken[] = '{';
                    } elseif ($curlyDepth > 0) {
                        // a nested curly brace - just push it
                        $thistoken[] = '{';
                        ++$curlyDepth;
                    } else {
                        // then starting an expression - save the out-of-expression string
                        if (count($thistoken) > 0) {
                            $_token = implode('', $thistoken);
                            $tokens[] = array(
                                $_token,
                                $offset,
                                'STRING'
                                );
                            $offset += strlen($_token);
                        }
                        $curlyDepth = 1;
                        $thistoken = array();
                        $thistoken[] = '{';
                    }
                    break;
                case '}':
                    // don't count this as an expression if the closing brace is preceded by whitespace
                    if ($j > 0 && preg_match('/\s|\n|\r/', substr($parts[$j - 1], -1, 1))) {
                        $thistoken[] = '}';
                    } elseif ($curlyDepth == 0) {
                        // just push the token
                        $thistoken[] = '}';
                    } else {
                        if ($inSQString || $inDQString) {
                            // just push the token
                            $thistoken[] = '}';
                        } else {
                            --$curlyDepth;
                            if ($curlyDepth == 0) {
                                // then closing expression
                                $thistoken[] = '}';
                                $_token = implode('', $thistoken);
                                $tokens[] = array(
                                    $_token,
                                    $offset,
                                    'EXPRESSION'
                                    );
                                $offset += strlen($_token);
                                $thistoken = array();
                            } else {
                                // just push the token
                                $thistoken[] = '}';
                            }
                        }
                    }
                    break;
                case '\'':
                    $thistoken[] = '\'';
                    if ($curlyDepth == 0) {
                        // only counts as part of a string if it is already within an expression
                    } else {
                        if ($inDQString) {
                            // then just push the single quote
                        } else {
                            if ($inSQString) {
                                $inSQString = false; // finishing a single-quoted string
                            } else {
                                $inSQString = true; // starting a single-quoted string
                            }
                        }
                    }
                    break;
                case '"':
                    $thistoken[] = '"';
                    if ($curlyDepth == 0) {
                        // only counts as part of a string if it is already within an expression
                    } else {
                        if ($inSQString) {
                            // then just push the double quote
                        } else {
                            if ($inDQString) {
                                $inDQString = false; // finishing a double-quoted string
                            } else {
                                $inDQString = true; // starting a double-quoted string
                            }
                        }
                    }
                    break;
                case '\\':
                    if ($j < ($count - 1)) {
                        $thistoken[] = $parts[$j++];
                        $thistoken[] = $parts[$j];
                    }
                    break;
                default:
                    $thistoken[] = $parts[$j];
                    break;
            }
        }
        if (count($thistoken) > 0) {
            $tokens[] = array(
                implode('', $thistoken),
                $offset,
                'STRING',
            );
        }
        return $tokens;
    }

    /**
     * Specify the survey  mode for this survey.  Options are 'survey', 'group', and 'question'
     * @param string $mode
     */
    public function SetSurveyMode($mode)
    {
        if (preg_match('/^group|question|survey$/', $mode)) {
            $this->surveyMode = $mode;
        }
    }


    /**
     * Pop a value token off of the stack
     * @return token
     */
    public function RDP_StackPop()
    {
        if (count($this->RDP_stack) > 0) {
            return array_pop($this->RDP_stack);
        } else {
            $this->RDP_AddError(self::gT("Tried to pop value off of empty stack"), null);
            return null;
        }
    }

    /**
     * Stack only holds values (number, string), not operators
     * @param array $token
     */
    public function RDP_StackPush(array $token)
    {
        if ($this->RDP_onlyparse) {
            // If only parsing, still want to validate syntax, so use "1" for all variables
            switch ($token[2]) {
                case 'DQ_STRING':
                case 'SQ_STRING':
                    $this->RDP_stack[] = array(1, $token[1], $token[2]);
                    break;
                case 'NUMBER':
                default:
                    $this->RDP_stack[] = array(1, $token[1], 'NUMBER');
                    break;
            }
        } else {
            $this->RDP_stack[] = $token;
        }
    }

    /**
     * Public call of RDP_Tokenize
     *
     * @param string $sSource : the string to tokenize
     * @param bool $bOnEdit : on edition, actually don't remove space
     * @return array
     */
    public function Tokenize($sSource, $bOnEdit)
    {
        return $this->RDP_Tokenize($sSource, $bOnEdit);
    }

    /**
     * Split the source string into tokens, removing whitespace, and categorizing them by type.
     *
     * @param string $sSource : the string to tokenize
     * @param bool $bOnEdit : on edition, actually don't remove space
     * @return array
     */
    private function RDP_Tokenize($sSource, $bOnEdit = false)
    {
        $cacheKey = 'RDP_Tokenize_' . $sSource . json_encode($bOnEdit);
        $value = EmCacheHelper::get($cacheKey);
        if ($value !== false) {
            return $value;
        }

        // $aInitTokens = array of tokens from equation, showing value and offset position.  Will include SPACE.
        if ($bOnEdit) {
                    $aInitTokens = preg_split($this->RDP_TokenizerRegex, $sSource, -1, (PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE));
        } else {
                    $aInitTokens = preg_split($this->RDP_TokenizerRegex, $sSource, -1, (PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE));
        }
        // $aTokens = array of tokens from equation, showing value, offsete position, and type.  Will not contain SPACE if !$bOnEdit, but will contain OTHER
        $aTokens = array();
        // Add token_type to $tokens:  For each token, test each categorization in order - first match will be the best.
        $countInitTokens = count($aInitTokens);
        for ($j = 0; $j < $countInitTokens; ++$j) {
            for ($i = 0; $i < count($this->RDP_CategorizeTokensRegex); ++$i) {
                $sToken = $aInitTokens[$j][0];
                if (preg_match($this->RDP_CategorizeTokensRegex[$i], $sToken)) {
                    if ($this->RDP_TokenType[$i] !== 'SPACE' || $bOnEdit) {
                        $aInitTokens[$j][2] = $this->RDP_TokenType[$i];
                        if ($this->RDP_TokenType[$i] == 'DQ_STRING' || $this->RDP_TokenType[$i] == 'SQ_STRING') {
                            // remove outside quotes
                            $sUnquotedToken = str_replace(array('\"', "\'", "\\\\"), array('"', "'", '\\'), substr($sToken, 1, -1));
                            $aInitTokens[$j][0] = $sUnquotedToken;
                        }
                        $aTokens[] = $aInitTokens[$j]; // get first matching non-SPACE token type and push onto $tokens array
                    }
                    break; // only get first matching token type
                }
            }
        }

        EmCacheHelper::set($cacheKey, $aTokens);
        return $aTokens;
    }

    /**
     * Show a table of allowable ExpressionScript Engine functions
     * @return string
     */
    static function ShowAllowableFunctions()
    {
        $em = new ExpressionManager();
        $output = "<div class='h3'>Functions Available within ExpressionScript Engine</div>\n";
        $output .= "<table border='1' class='table'><tr><th>Function</th><th>Meaning</th><th>Syntax</th><th>Reference</th></tr>\n";
        foreach ($em->RDP_ValidFunctions as $name => $func) {
            $output .= "<thead><tr><th>" . $name . "</th><th>" . $func[2] . "</th><th>" . $func[3] . "</th><th>";

        // 508 fix, don't output empty anchor tags
            if ($func[4]) {
                $output .= "<a href='" . $func[4] . "'>" . $func[4] . "</a>";
            }

            $output .= "&nbsp;</td></tr>\n";
        }
        $output .= "</table>\n";
        return $output;
    }

    /**
     * Show a table of allowable ExpressionScript Engine functions
     * @return string
     */
    static function GetAllowableFunctions()
    {
        $em = new ExpressionManager();
        return $em->RDP_ValidFunctions;
    }

    /**
     * Show a translated string for admin user, always in admin language #12208
     * public for geterrors_exprmgr_regexMatch function only
     * @param string $string to translate
     * @param string $sEscapeMode Valid values are html (this is the default, js and unescaped)
     * @return string : translated string
     */
    public static function gT($string, $sEscapeMode = 'html')
    {
        return gT(
            $string,
            $sEscapeMode,
            Yii::app()->session->get('adminlang', App()->getConfig("defaultlang"))
        );
    }
}

/**
 * Used by usort() to order Error tokens by their position within the string
 * This must be outside of the class in order to work in PHP 5.2
 * @param array $a
 * @param array $b
 * @return int
 */
function cmpErrorTokens($a, $b)
{
    if (is_null($a[1])) {
        if (is_null($b[1])) {
            return 0;
        }
        return 1;
    }
    if (is_null($b[1])) {
        return -1;
    }
    if ($a[1][1] == $b[1][1]) {
        return 0;
    }
    return ($a[1][1] < $b[1][1]) ? -1 : 1;
}

/**
 * @param EMWarningInterface $a
 * @param EMWarningInterface $b
 * @return int
 * @todo Unify errors and warnings with a EMErrorComparableInterface
 */
function cmpWarningTokens(EMWarningInterface $a, EMWarningInterface $b)
{
    $tokenA = $a->getToken();
    $tokenB = $b->getToken();

    if (is_null($tokenA)) {
        if (is_null($tokenB)) {
            return 0;
        }
        return 1;
    }
    if (is_null($tokenB)) {
        return -1;
    }

    if ($tokenA[1] == $tokenB[1]) {
        return 0;
    }
    return ($tokenA[1] < $tokenB[1]) ? -1 : 1;
}

/**
 * Count the number of answered questions (non-empty)
 * @param array $args
 * @return int
 */
function exprmgr_count($args)
{
    $j = 0; // keep track of how many non-null values seen
    foreach ($args as $arg) {
        if ($arg != '') {
            ++$j;
        }
    }
    return $j;
}

/**
 * Count the number of answered questions (non-empty) which match the first argument
 * @param array $args
 * @return int
 */
function exprmgr_countif($args)
{
    $j = 0; // keep track of how many non-null values seen
    $match = array_shift($args);
    foreach ($args as $arg) {
        if ($arg == $match) {
            ++$j;
        }
    }
    return $j;
}

/**
 * Count the number of answered questions (non-empty) which meet the criteria (arg op value)
 * @param array $args
 * @return int
 */
function exprmgr_countifop($args)
{
    $j = 0;
    $op = array_shift($args);
    $value = array_shift($args);
    foreach ($args as $arg) {
        switch ($op) {
            case '==':
            case 'eq':
                if ($arg == $value) {
                    ++$j;
                }
                break;
            case '>=':
            case 'ge':
                if ($arg >= $value) {
                    ++$j;
                }
                break;
            case '>':
            case 'gt':
                if ($arg > $value) {
                    ++$j;
                }
                break;
            case '<=':
            case 'le':
                if ($arg <= $value) {
                    ++$j;
                }
                break;
            case '<':
            case 'lt':
                if ($arg < $value) {
                    ++$j;
                }
                break;
            case '!=':
            case 'ne':
                if ($arg != $value) {
                    ++$j;
                }
                break;
            case 'RX':
                try {
                    if (@preg_match($value, (string) $arg)) {
                        ++$j;
                    }
                } catch (Exception $e) {
                    // Do nothing
                }
                break;
        }
    }
    return $j;
}
/**
 * Find position of first occurrence of unicode string in a unicode string, case insensitive
 * @param string $haystack : checked string
 * @param string $needle : string to find
 * @param $offset : offset
 * @return int|false : position or false if not found
 */
function exprmgr_stripos($haystack, $needle, $offset = 0)
{
    if ($offset > mb_strlen($haystack)) {
            return false;
    }
    return mb_stripos($haystack, $needle, $offset, 'UTF-8');
}
/**
 * Finds first occurrence of a unicode string within another, case-insensitive
 * @param string $haystack : checked string
 * @param string $needle : string to find
 * @param boolean $before_needle : portion to return
 * @return string|false
 */
function exprmgr_stristr($haystack, $needle, $before_needle = false)
{
    return mb_stristr($haystack, $needle, $before_needle, 'UTF-8');
}
/**
 * Get unicode string length
 * @param string $string
 * @return int
 */
function exprmgr_strlen($string)
{
    return mb_strlen($string, 'UTF-8');
}
/**
 * Find position of first occurrence of unicode string in a unicode string
 * @param string $haystack : checked string
 * @param string $needle : string to find
 * @param int $offset : offset
 * @return int|false : position or false if not found
 */
function exprmgr_strpos($haystack, $needle, $offset = 0)
{
    if ($offset > mb_strlen($haystack)) {
            return false;
    }
    return mb_strpos($haystack, $needle, $offset, 'UTF-8');
}
/**
 * Finds first occurrence of a unicode string within another
 * @param string $haystack : checked string
 * @param string $needle : string to find
 * @param boolean $before_needle : portion to return
 * @return string|false
 */
function exprmgr_strstr($haystack, $needle, $before_needle = false)
{
    return mb_strstr($haystack, $needle, $before_needle, 'UTF-8');
}
/**
 * Make an unicode string lowercase
 * @param string $string
 * @return string
 */
function exprmgr_strtolower($string)
{
    return mb_strtolower($string, 'UTF-8');
}
/**
 * Make an unicode string uppercase
 * @param string $string
 * @return string
 */
function exprmgr_strtoupper($string)
{
    return mb_strtoupper($string, 'UTF-8');
}
/**
 * Get part of unicode string
 * @param string $string
 * @param int $start
 * @param int $end
 * @return string
 */
function exprmgr_substr($string, $start, $end = null)
{
    return mb_substr($string, $start, $end, 'UTF-8');
}
/**
 * Sum of values of answered questions which meet the criteria (arg op value)
 * @param array $args
 * @return int
 */
function exprmgr_sumifop($args)
{
    $result = 0;
    $op = array_shift($args);
    $value = array_shift($args);
    foreach ($args as $arg) {
        switch ($op) {
            case '==':
            case 'eq':
                if ($arg == $value) {
                    $result += $arg;
                }
                break;
            case '>=':
            case 'ge':
                if ($arg >= $value) {
                    $result += $arg;
                }
                break;
            case '>':
            case 'gt':
                if ($arg > $value) {
                    $result += $arg;
                }
                break;
            case '<=':
            case 'le':
                if ($arg <= $value) {
                    $result += $arg;
                }
                break;
            case '<':
            case 'lt':
                if ($arg < $value) {
                    $result += $arg;
                }
                break;
            case '!=':
            case 'ne':
                if ($arg != $value) {
                    $result += $arg;
                }
                break;
            case 'RX':
                try {
                    if (@preg_match($value, (string) $arg)) {
                        $result += $arg;
                    }
                } catch (Exception $e) {
                    // Do nothing
                }
                break;
        }
    }
    return $result;
}

/**
 * Validate a Gregorian date
 * @see https://www.php.net/checkdate
 * Check if all params are valid before send it to PHP checkdate to avoid PHP Warning
 *
 * @param mixed $month
 * @param mixed $day
 * @param mixed $year
 * @return boolean
 */
function exprmgr_checkdate($month, $day, $year)
{
    if (
        (!ctype_digit($month) && !is_int($month))
        || (!ctype_digit($day) && !is_int($day))
        || (!ctype_digit($year) && !is_int($year))
    ) {
        return false;
    }
    return checkdate(intval($month), intval($day), intval($year));
}

/**
 * Find the closest matching Numerical input values in a list an replace it by the
 * corresponding value within another list
 *
 * @author Johannes Weberhofer, 2013
 *
 * @param double $fValueToReplace
 * @param integer $iStrict - 1 for exact matches only otherwise interpolation the
 *          closest value should be returned
 * @param string $sTranslateFromList - comma seperated list of numeric values to translate from
 * @param string $sTranslateToList - comma seperated list of numeric values to translate to
 * @return integer|null
 */
function exprmgr_convert_value($fValueToReplace, $iStrict, $sTranslateFromList, $sTranslateToList)
{
    if ((is_numeric($fValueToReplace)) && ($iStrict != null) && ($sTranslateFromList != null) && ($sTranslateToList != null)) {
        $aFromValues = explode(',', $sTranslateFromList);
        $aToValues = explode(',', $sTranslateToList);
        if ((count($aFromValues) > 0) && (count($aFromValues) == count($aToValues))) {
            $fMinimumDiff = null;
            $iNearestIndex = 0;
            for ($i = 0; $i < count($aFromValues); $i++) {
                if (!is_numeric($aFromValues[$i])) {
                    // break processing when non-numeric variables are about to be processed
                    return null;
                }
                $fCurrentDiff = abs($aFromValues[$i] - $fValueToReplace);
                if ($fCurrentDiff === 0) {
                    return $aToValues[$i];
                } elseif ($i === 0) {
                    $fMinimumDiff = $fCurrentDiff;
                } elseif ($fMinimumDiff > $fCurrentDiff) {
                    $fMinimumDiff = $fCurrentDiff;
                    $iNearestIndex = $i;
                }
            }
            if ($iStrict != 1) {
                return $aToValues[$iNearestIndex];
            }
        }
    }
    return null;
}

/**
 * Return format a local time/date
 * Need to test if timestamp is numeric (else E_WARNING with debug>0)
 * @param string $format
 * @param int $timestamp
 * @return string|false
 * @link http://php.net/function.date.php
 */
function exprmgr_date($format, $timestamp = null)
{
    $timestamp = $timestamp ?? time();
    if (!is_numeric($timestamp)) {
        return false;
    }
    return date($format, $timestamp);
}

function exprmgr_abs($num)
{
    if (!is_numeric($num)) {
        return false;
    }

    // Trying to cast either to int or float, depending on the value.
    $num = $num + 0;
    return abs($num);
}

/**
 * Calculate the sum of values in an array
 * @see https://bugs.limesurvey.org/view.php?id=19897
 * @see https://www.php.net/manual/en/function.array-sum.php
 * Like php 8.1 and before : Ignore array or object, cast to float string and cast to int other.
 * @param array $args
 * @return float
 */
function exprmgr_array_sum($args)
{
    $args = array_map(function ($arg) {
        if (is_int($arg) || is_float($arg)) {
            return $arg;
        }
        if (is_string($arg)) {
            return floatval($arg);
        }
        if (is_array($arg) || is_object($arg)) {
            return 0;
        }
        return intval($arg);
    }, $args);
    return array_sum($args);
}

/**
 * If $test is true, return $iftrue, else return $iffalse
 * @param mixed $testDone
 * @param mixed $iftrue
 * @param mixed $iffalse
 * @return mixed
 */
function exprmgr_if($testDone, $iftrue, $iffalse = '')
{
    if ($testDone) {
        return $iftrue;
    }
    return $iffalse;
}

/**
 * Return true if the variable is an integer for LimeSurvey
 * Allow usage of numeric answercode as int
 * Can not use is_int due to SQL DECIMAL system.
 * @param string $arg
 * @return integer
 * @link http://php.net/is_int#82857
 */
function exprmgr_int($arg)
{
    if (strpos($arg, ".")) {
        // DECIMAL from SQL return always .00000000, the remove all 0 and one . , see #09550
        $arg = preg_replace("/\.$/", "", rtrim(strval($arg), "0"));
    }
    // Allow 000 for value
    // Disallow '' (and false) @link https://bugs.limesurvey.org/view.php?id=17950
    return (preg_match("/^-?\d+$/", $arg));
}

/**
 * Join together $args[0-N] with ', '
 * @param array $args
 * @return string
 */
function exprmgr_list($args)
{
    $result = "";
    $j = 1; // keep track of how many non-null values seen
    foreach ($args as $arg) {
        if ($arg != '') {
            if ($j > 1) {
                $result .= ', ' . $arg;
            } else {
                $result .= $arg;
            }
            ++$j;
        }
    }
    return $result;
}

/**
 * Implementation of listifop( $cmpAttr, $op, $value, $retAttr, $glue, $sgqa1, ..., sgqaN )
 * Return a list of retAttr from sgqa1...sgqaN which pass the critiera (cmpAttr op value)
 * @param array $args
 * @return string
 */
function exprmgr_listifop($args)
{
    $result = "";
    $cmpAttr = array_shift($args);
    $op = array_shift($args);
    $value = array_shift($args);
    $retAttr = array_shift($args);
    $glue = array_shift($args);

    $validAttributes = "/" . LimeExpressionManager::getRegexpValidAttributes() . "/";
    if (! preg_match($validAttributes, (string) $cmpAttr)) {
        return $cmpAttr . " not recognized ?!";
    }
    if (! preg_match($validAttributes, (string) $retAttr)) {
        return $retAttr . " not recognized ?!";
    }

    foreach ($args as $sgqa) {
        $cmpVal = LimeExpressionManager::GetVarAttribute($sgqa, $cmpAttr, null, -1, -1);
        $match = false;

        switch ($op) {
            case '==':
            case 'eq':
                $match = ($cmpVal == $value);
                break;
            case '>=':
            case 'ge':
                $match = ($cmpVal >= $value);
                break;
            case '>':
            case 'gt':
                $match = ($cmpVal > $value);
                break;
            case '<=':
            case 'le':
                $match = ($cmpVal <= $value);
                break;
            case '<':
            case 'lt':
                $match = ($cmpVal < $value);
                break;
            case '!=':
            case 'ne':
                $match = ($cmpVal != $value);
                break;
            case 'RX':
                try {
                    $match = preg_match($value, (string) $cmpVal);
                } catch (Exception $ex) {
                    return "Invalid RegEx";
                }
                break;
        }

        if ($match) {
            $retVal = LimeExpressionManager::GetVarAttribute($sgqa, $retAttr, null, -1, -1);
            if ($result != "") {
                $result .= $glue;
            }
            $result .= $retVal;
        }
    }

    return $result;
}

/**
 * return log($arg[0],$arg[1]=e)
 * @param array $args
 * @return float
 */
function exprmgr_log($args)
{
    if (count($args) < 1) {
        return NAN;
    }
    $number = $args[0];
    if (!is_numeric($number)) {
        return NAN;
    }
    $base = $args[1] ?? exp(1);
    if (!is_numeric($base)) {
        return NAN;
    }
    if (floatval($base) <= 0) {
        return NAN;
    }
    return log($number, $base);
}
/**
 * Get Unix timestamp for a date : false if parameters is invalid.
 * Get default value for unset (or null) value
 * E_NOTICE if arguments are not numeric (debug>0), then test it before
 * @param int $hour
 * @param int $minute
 * @param int $second
 * @param int $month
 * @param int $day
 * @param int $year
 * @return int|boolean
 */
function exprmgr_mktime($hour = null, $minute = null, $second = null, $month = null, $day = null, $year = null)
{
    $hour = $hour ?? date("H");
    $minute = $minute ?? date("i");
    $second = $second ?? date("s");
    $month = $month ?? date("n");
    $day = $day ?? date("j");
    $year = $year ?? date("Y");
    $hour = $hour ?? date("H");
    $iInvalidArg = count(array_filter(array($hour, $minute, $second, $month, $day, $year), function ($timeValue) {
        return !is_numeric($timeValue); /* This allow get by string like "01.000" , same than javascript with 2.72.6 and default PHP(5.6) function*/
    }));
    if ($iInvalidArg) {
        return false;
    }
    return mktime($hour, $minute, $second, $month, $day, $year);
}

/**
 * Join together $args[N]
 * @param array $args
 * @return string
 */
function exprmgr_join($args)
{
    return implode("", $args);
}

/**
 * Join together $args[1-N] with $arg[0]
 * @param array $args
 * @return string
 */
function exprmgr_implode($args)
{
    if (count($args) <= 1) {
        return "";
    }
    $joiner = array_shift($args);
    return implode($joiner, $args);
}

/**
 * Return true if the variable is NULL or blank.
 * @param null|string|boolean $arg
 * @return boolean
 */
function exprmgr_empty($arg)
{
    if ($arg === null || $arg === "" || $arg === false) {
        return true;
    }
    return false;
}

/**
 * Compute the Sample Standard Deviation of a set of numbers ($args[0-N])
 * @param array $args
 * @return float
 */
function exprmgr_stddev($args)
{
    $vals = array();
    foreach ($args as $arg) {
        if (is_numeric($arg)) {
            $vals[] = $arg;
        }
    }
    $count = count($vals);
    if ($count <= 1) {
        return 0; // what should default value be?
    }
    $sum = 0;
    foreach ($vals as $val) {
        $sum += $val;
    }
    $mean = $sum / $count;

    $sumsqmeans = 0;
    foreach ($vals as $val) {
        $sumsqmeans += ($val - $mean) * ($val - $mean);
    }
    $stddev = sqrt($sumsqmeans / ($count - 1));
    return $stddev;
}

/**
 * Javascript equivalent does not cope well with ENT_QUOTES and related PHP constants, so set default to ENT_QUOTES
 * @param string $string
 * @return string
 */
function expr_mgr_htmlspecialchars($string)
{
    return htmlspecialchars($string, ENT_QUOTES);
}

/**
 * Javascript equivalent does not cope well with ENT_QUOTES and related PHP constants, so set default to ENT_QUOTES
 * @param string $string
 * @return string
 */
function expr_mgr_htmlspecialchars_decode($string)
{
    return htmlspecialchars_decode($string, ENT_QUOTES);
}

/**
 * Return true if $input matches the regular expression $pattern
 * @param string $pattern
 * @param string $input
 * @return boolean
 */
function exprmgr_regexMatch($pattern, $input)
{
    // Test the regexp pattern agains null : must always return 0, false if error happen
    if (@preg_match($pattern . 'u', '') === false) {
        return false; // invalid : true or false ?
    }
    // 'u' is the regexp modifier for unicode so that non-ASCII string will be validated properly
    return preg_match($pattern . 'u', $input);
}
/**
 * Return error information from pattern of regular expression $pattern
 * @param string $pattern
 * @param string $input
 * @return string|null
 */
function geterrors_exprmgr_regexMatch($pattern, $input)
{
    // @todo : use set_error_handler to get the preg_last_error
    if (@preg_match($pattern . 'u', '') === false) {
        return sprintf(ExpressionManager::gT('Invalid PERL Regular Expression: %s'), htmlspecialchars($pattern));
    }
}

/**
 * Display number with comma as radix separator, if needed
 * @param string $value
 * @return string
 */
function exprmgr_fixnum($value)
{
    if (LimeExpressionManager::usingCommaAsRadix()) {
        $newval = implode(',', explode('.', $value));
        return $newval;
    }
    return $value;
}
/**
 * Returns true if all non-empty values are unique
 * @param array $args
 * @return boolean
 */
function exprmgr_unique($args)
{
    $uniqs = array();
    foreach ($args as $arg) {
        if (trim((string) $arg) == '') {
            continue; // ignore blank answers
        }
        if (isset($uniqs[$arg])) {
            return false;
        }
        $uniqs[$arg] = 1;
    }
    return true;
}
