<?php
/**
* Web Application Component Toolkit
*
* @link http://www.phpwact.org/
*
* @author Wact Development Team
* @link http://www.phpwact.org/team
*
* @copyright Copyright 2006, Jeff Moore
* @license http://opensource.org/licenses/mit-license.php MIT
*/

/**
* Parse an text string composed of text and expressions.
*/
class Wact_Template_Compiler_Expression_Parser {

    /**
    */
    protected $compilingContext;

    /**
    */
    protected $text;

    /**
    */
    protected $position;

    /**
    */
    protected $length;

    /**
    * Construct this parser
    */
    function __construct($context) {
        $this->compilingContext = $context;
    }

    /**
    */
    protected function getToken($pattern) {
        if (preg_match($pattern, $this->text, $match, PREG_OFFSET_CAPTURE, $this->position)) {
            $this->position += strlen($match[0][0]);
            return $match[1][0];
        } else {
            return FALSE;
        }
    }

    /**
    */
    protected function parseArrayConstant() {

        $array = array();

        // Short circuit makes later parsing easier
        if ($this->getToken('/\G\s*(\])/u')) {
            return new Wact_Template_Compiler_Expression_Node_Constant(array());
        }

        do {

            $keyExpr = $this->parseExpression();

            if ($this->getToken('/\G\s*(=>)/u')) {
                if (!$keyExpr->isConstant()) {
                    throw new Exception('Array Keys must be constant expressions.');
                }

                $key = $keyExpr->getValue();
    
                if (gettype($key) != 'integer' && gettype($key) != 'string') {
                    throw new Exception('array keys must be strings or integers');
                }
    
                $array[$key] = $this->parseExpression();
            } else {
                // Single value element
                $array[] = $keyExpr;
            }

        } while ($this->getToken('/\G\s*(,)/u'));

        if ($this->getToken('/\G\s*(\])/u')) {
            return new Wact_Template_Compiler_Expression_Node_Array($array);
        } else {
            throw new exception("expecting more list values");
        }

    }

    /**
    */
    protected function parseFunction($formatterName) {
        $formatterInfo = $this->compilingContext->getFormatterInfo($formatterName);
        if (!$formatterInfo) {
            throw new Exception("Filter not found $formatterName");
        }
        
        $class = $formatterInfo->getName();
        $formatter = new $class();
        $count=0;
        if (!$this->getToken('/\G\s*(\))/u')) {
            do {
                $param = $this->parseExpression();
                $formatter->registerParameter($param);
                $count++;
            } while ($token = $this->getToken('/\G\s*(,)/u'));

            if (!$this->getToken('/\G\s*(\))/u')) {
                throw new Exception('Expecting ).');
            }
        }
        if ($count < $formatterInfo->minParameterCount) {
            throw new Exception('Too few parameters');
        }
        if ($count > $formatterInfo->maxParameterCount) {
            throw new Exception('Too many parameters');
        }
        return $formatter;
    }

    /**
    */
    protected function parsePrimary() {

        $token = $this->getToken('/\G\s*(#|\^|-|"|\'|\[|\(|[0-9]+|[A-Za-z][A-Za-z0-9_]*)/u');
        if ($token === FALSE) {
            throw new Exception("expecting primary.");
        }

        if ($token == '-') {
            return new Wact_Template_Compiler_Expression_Node_UnaryMinus($this->parseDereferenced());
        } else if ($token == '(') {
            $expr = $this->parseExpression();
            if ($this->getToken('/\G\s*(\))/u')) {
                return $expr;
            } else {
                throw new Exception('Expecting ).');
            }
        } else if ($token == '^') {
            $count = 1;
            while ($token = $this->getToken('/\G\s*(\^)/u')) {
                $count++;
            }
            if (!($token = $this->getToken('/\G\s*([A-Za-z][A-Za-z0-9_]*)/u'))) {
                throw new Exception("expecting identifier.");
            }
            return new Wact_Template_Compiler_Expression_Node_Parent($token, $count);
        } else if ($token == '#') {
            if (!($token = $this->getToken('/\G\s*([A-Za-z][A-Za-z0-9_]*)/u'))) {
                throw new Exception("expecting identifier.");
            }
            return new Wact_Template_Compiler_Expression_Node_Root($token);
        } else if ($token == '"' || $token == "'") {
            $string = $this->getToken('/\G([^' . $token . ']*)' . $token . '/u');
            if ($string !== FALSE) {
                return new Wact_Template_Compiler_Expression_Node_Constant($string);
            } else {
                throw new Exception("Expecting a string literal.");
            }
        } else if ($token == '[') {
            return $this->parseArrayConstant();
        } else if (ctype_digit($token)) {
            if ($decimalToken = $this->getToken('/\G\.(\d+)/u')) {
                return new Wact_Template_Compiler_Expression_Node_Constant(floatval($token . '.' . $decimalToken));
            } else {
                return new Wact_Template_Compiler_Expression_Node_Constant(intval($token));
            }
        } else if (strcasecmp($token, 'and') == 0) {
            throw new Exception('reserved');
        } else if (strcasecmp($token, 'or') == 0) {
            throw new Exception('reserved');
        } else if (strcasecmp($token, 'not') == 0) {
            $expr = $this->parseExpression();
            return new Wact_Template_Compiler_Expression_Node_Not($expr);
        } else if (strcasecmp($token, 'null') == 0) {
            return new Wact_Template_Compiler_Expression_Node_Constant(NULL);
        } else if (strcasecmp($token, 'true') == 0) {
            return new Wact_Template_Compiler_Expression_Node_Constant(TRUE);
        } else if (strcasecmp($token, 'false') == 0) {
            return new Wact_Template_Compiler_Expression_Node_Constant(FALSE);
        } else if ($this->getToken('/\G\s*(\()/u')) {
            return $this->parseFunction($token);
        }
        return new Wact_Template_Compiler_Expression_Node_Data($token);

    }

    /**
    * dereferenced := primary { '.' identifier }
    */
    protected function parseDereferenced() {
        $primary = $this->parsePrimary();

        $path = array();
        while ($token = $this->getToken('/\G\s*\.\s*([A-Za-z][A-Za-z0-9_]*)/u')) {
            $path[] = $token;
        }

        if (empty($path)) {
            return $primary;
        } else {
            return new Wact_Template_Compiler_Expression_Node_Dereference($primary, implode($path, '.'));
        }

    }

    /**
    * term := primary { '*' primary | '/' primary | '%' primary }
    */
    protected function parseTerm() {

        $term = $this->parseDereferenced();

        while ($token = $this->getToken('/\G\s*(\*|\/|%)/u')) {
            $deref = $this->parseDereferenced();

            if ($token == '*') {
                $term = new Wact_Template_Compiler_Expression_Node_Multiplication($term, $deref);
            } else if ($token == '/') {
                $term = new Wact_Template_Compiler_Expression_Node_Division($term, $deref);
            } else {
                $term = new Wact_Template_Compiler_Expression_Node_Modulo($term, $deref);
            }

        }

        return $term;
    }

    /**
    * sum := term { '+' term | '-' term | '&' term }
    */
    protected function parseSum() {

        $sum = $this->parseTerm();

        while ($token = $this->getToken('/\G\s*(\+|-|&)/u')) {
            $term = $this->parseTerm();

            if ($token == '+') {
                $sum = new Wact_Template_Compiler_Expression_Node_Addition($sum, $term);
            } else if ($token == '-') {
                $sum = new Wact_Template_Compiler_Expression_Node_Subtraction($sum, $term);
            } else {
                $sum = new Wact_Template_Compiler_Expression_Node_Concatination($sum, $term);
            }

        }

        return $sum;
    }

    /**
    * comparison :=
    */
    protected function parseComparison() {

        $comparison = $this->parseSum();

        while ($token = $this->getToken('/\G\s*(>=|<=|==|!=|>|<)/u')) {
            $sum = $this->parseSum();

            if ($token == '==') {
                $comparison = new Wact_Template_Compiler_Expression_Node_EqualTo($comparison, $sum);
            } else if ($token == '!=') {
                $comparison = new Wact_Template_Compiler_Expression_Node_NotEqual($comparison, $sum);
            } else if ($token == '<') {
                $comparison = new Wact_Template_Compiler_Expression_Node_LessThan($comparison, $sum);
            } else if ($token == '>') {
                $comparison = new Wact_Template_Compiler_Expression_Node_GreaterThan($comparison, $sum);
            } else if ($token == '<=') {
                $comparison = new Wact_Template_Compiler_Expression_Node_LessThanEqTo($comparison, $sum);
            } else {
                $comparison = new Wact_Template_Compiler_Expression_Node_GreaterThanEqTo($comparison, $sum);
            }

        }

        return $comparison;
    }

    /**
    * logical := comparison { 'and' comparison | 'or' comparison }
    */
    protected function parseLogical() {

        $logical = $this->parseComparison();

        while ($token = $this->getToken('/\G\s*(or|and)/u')) {
            $comparison = $this->parseComparison();

            if (strcasecmp($token, 'and') == 0) {
                $logical = new Wact_Template_Compiler_Expression_Node_And($logical, $comparison);
            } else {
                $logical = new Wact_Template_Compiler_Expression_Node_Or($logical, $comparison);
            }

        }

        return $logical;
    }

    /**
    * expression := logical { '|' name logical* }*
    */
    protected function parseFormatted() {
        $formatted = $this->parseLogical();

        while ($token = $this->getToken('/\G\s*\|\s*([a-zA-z]+[a-zA-Z0-9_]*)/u')) {
            $formatterName = $token;
            $formatterInfo = $this->compilingContext->getFormatterInfo($formatterName);
            if ($formatterInfo) {
                $class = $formatterInfo->getName();
                $formatter = new $class();
                $formatter->registerParameter($formatted);
                $count=1;
                if ($token = $this->getToken('/\G\s*(:)/u')) {
                    do {
                        $param = $this->parseLogical();
                        $formatter->registerParameter($param);
                        $count++;
                    } while ($token = $this->getToken('/\G\s*(,)/u'));
                }
                if ($count < $formatterInfo->minParameterCount) {
                    throw new Exception('Too few parameters');
                }
                if ($count > $formatterInfo->maxParameterCount) {
                    throw new Exception('Too many parameters');
                }
                $formatted = $formatter;
            } else {
                throw new Exception("Filter not found $token");
            }
        }
        return $formatted;
    }
    
    /**
    * expression := logical
    */
    protected function parseExpression() {
        return $this->parseFormatted();
    }

    /**
    * Parse text for expressions and emit a stream of events for expression fragments
    */
    public function parseFragments(Wact_Template_Compiler_Expression_Observer $observer, $text, $defaultRoot = NULL) {
        $exprStart = strpos($text, '{$');
        if ($exprStart === FALSE) {
            // Shortcut the most common case
            $observer->addLiteral($text);
            return;
        }

        $this->length = strlen($text);

        if ($this->length == 0) {
            return;
        }

        $this->text = $text;
        $this->position = 0;
        $fragmentStart = 0;
        $isFragmented = FALSE;

        do {
            if ($exprStart > $fragmentStart) {
                $segment = substr($this->text, $fragmentStart, $exprStart - $fragmentStart);
                if (!$isFragmented) {
                    $observer->beginFragments();
                    $isFragmented = TRUE;
                }
                $observer->addLiteralFragment($segment);
                $fragmentStart = $exprStart;
            }

            $this->position = $exprStart + 2;

            $expression = $this->parseExpression();

            if (!$this->getToken('/\G\s*(\!raw)/u') && $defaultRoot) {
                $expression = new $defaultRoot($expression);
            }

            if (!$this->getToken('/\G\s*(\})/u')) {
                throw new Exception('Expection end of expression.');
            }

            if ($this->position >= $this->length && $fragmentStart == 0 ) {
                $observer->addExpression($expression);
                return;
            } else {
                if (!$isFragmented) {
                    $observer->beginFragments();
                    $isFragmented = TRUE;
                }
                $observer->addExpressionFragment($expression);
            }

            $fragmentStart = $this->position;
            $exprStart = strpos($this->text, '{$', $this->position);
        } while (($exprStart !== FALSE));

        if ($this->position < $this->length) {
            $observer->addLiteralFragment(substr($this->text, $this->position));
        }

        if ($isFragmented) {
            $observer->endFragments();
        }

    }

    /**
    * Parse text for an undelimited expression and return an expression tree
    */
    public function parse($text) {
        $this->length = strlen($text);

        if ($this->length == 0) {
            return;
        }

        $this->text = $text;
        $this->position = 0;

        $expression = $this->parseExpression();

        if ($this->position < $this->length &&
            preg_match('/\G\s*$/u', $this->text, $match, PREG_OFFSET_CAPTURE, $this->position)) {
                throw new Exception('Expection end of expression.');
        }

        return $expression;

    }
}

?>