/**
 * This class parses bind template format expressions.
 * @private
 */
Ext.define('Ext.app.bind.Parser', {
    extend: 'Ext.parse.Parser',
 
    requires: [
        'Ext.util.Format'
    ],
 
    infix: {
        ':': {
            priority: 70, // bind tighter than multiplication
 
            //<debug>
            dump: function() {
                var me = this,
                    ret = {
                        at: me.at,
                        arity: me.arity,
                        value: me.value,
                        operand: me.operand.dump(),
                        fmt: []
                    },
                    fmt = me.fmt,
                    i;
 
                for (= 0; i < fmt.length; ++i) {
                    ret.fmt.push(fmt[i].dump());
                }
 
                return ret;
            },
            //</debug>
 
            led: function(left) {
                // We parse a sequence of ":" separated formatter expressions (like a
                // traditional "," operator) and gather the sequence in our "fmt" array
                var me = this;
 
                me.arity = 'formatter';
                me.operand = left;
                me.fmt = me.parser.parseFmt();
 
                return me;
            }
        },
 
        '?': {
            priority: 20,
 
            led: function(left) {
                var me = this,
                    parser = me.parser,
                    symbol = parser.symbols[':'],
                    temp;
 
                me.condition = left;
 
                // temporarily set priority of `:` symbol to 0
                temp = symbol.priority;
                symbol.priority = 0;
 
                me.tv = parser.parseExpression(0);
                me.parser.advance(':');
 
                // restore priority of `:`
                symbol.priority = temp;
 
                me.fv = parser.parseExpression(0);
                me.arity = 'ternary';
 
                return me;
            }
        }
    },
 
    symbols: {
        '(': {
            nud: function() {
                // Handles parenthesized expressions
                var parser = this.parser,
                    symbol = parser.symbols[':'],
                    ret, temp;
 
                // temporarily set priority of `:` symbol to 70 to correctly extract formatters 
                // inside parens
                temp = symbol.priority;
                symbol.priority = 70;
                ret = parser.parseExpression();
 
                parser.advance(")");
                // restore priority of `:`
                symbol.priority = temp;
 
                return ret;
            }
 
        }
    },
 
    prefix: {
        '@': 0
    },
 
    tokenizer: {
        operators: {
            '@': 'at',
            '?': 'qmark',
            '===': 'feq',
            '!==': 'fneq',
            '==': 'eq',
            '!=': 'neq',
            '<': 'lt',
            '<=': 'lte',
            '>': 'gt',
            '>=': 'gte',
            '&&': 'and',
            '||': 'or'
        }
    },
 
    /**
     * Parses the expression from the current position and compiles it as a function.
     * The expression tokens are stored in the provided arguments.
     *
     * Called by Ext.app.bind.Template.
     *
     * @param {Array} tokens 
     * @param {Object} tokensMaps 
     * @return {Function} 
     */
    compileExpression: function(tokens, tokensMaps) {
        var me = this,
            debug, fn;
 
        me.tokens = tokens;
        me.tokensMap = tokensMaps;
 
        //<debug>
        debug = me.token.value === '@' && me.tokenizer.peek();
 
        if (debug) {
            debug = debug.value === 'debugger';
 
            if (debug) {
                me.advance();
                me.advance();
            }
        }
        //</debug>
 
        fn = me.parseSlot(me.parseExpression(), debug);
 
        me.tokens = me.tokensMap = null;
 
        return fn;
    },
 
    /**
     * Parses the chained format functions and compiles them as a function.
     *
     * Called by the grid column formatter.
     *
     * @return {Function} 
     */
    compileFormat: function() {
        var me = this,
            fn;
 
        //<debug>
        try {
        //</debug>
            fn = me.parseSlot({
                arity: 'formatter',
                fmt: me.parseFmt(),
                operand: {
                    arity: 'ident',
                    value: 'dummy'
                }
            });
            me.expect('(end)');
        //<debug>
        }
        catch (e) {
            Ext.raise('Invalid format expression: "' + me.tokenizer.text + '"');
        }
        //</debug>
 
        return fn;
    },
 
    privates: {
        // Chrome really likes "new Function" to realize the code block (as in it is
        // 2x-3x faster to call it than using eval), but Firefox chokes on it badly.
        // IE and Opera are also fine with the "new Function" technique.
        useEval: Ext.isGecko,
        escapeRe: /(["'\\])/g,
 
        /**
         * Parses a series of ":" delimited format expressions.
         * @return {Ext.parse.Symbol[]} 
         * @private
         */
        parseFmt: function() {
            // We parse a sequence of ":" separated formatter expressions (like a
            // traditional "," operator)
            var me = this,
                fmt = [],
                priority = me.symbols[':'].priority,
                expr;
 
            do {
                if (fmt.length) {
                    me.advance();
                }
 
                expr = me.parseExpression(priority);
 
                if (expr.isIdent || expr.isInvoke) {
                    fmt.push(expr);
                }
                else {
                    me.syntaxError(expr.at, 'Expected formatter name');
                }
            } while (me.token.id === ':');
 
            return fmt;
        },
 
        /**
         * Parses the expression tree and compiles it as a function
         *
         * @param expr
         * @param {Boolean} debug 
         * @return {Function} 
         * @private
         */
        parseSlot: function(expr, debug) {
            var me = this,
                defs = [],
                body = [],
                tokens = me.tokens || [],
                fn, code, i, length, temp;
 
            me.definitions = defs;
            me.body = body;
 
            body.push('return ' + me.compile(expr) + ';');
 
            // now we have the tokens
            length = tokens.length;
            code = 'var fm = Ext.util.Format,\nme,';
            temp = 'var a = Ext.Array.from(values);\nme = scope;\n';
 
            if (tokens.length) {
                for (= 0; i < length; i++) {
                    code += 'v' + i + ((=== length - 1) ? ';' : ',');
                    temp += 'v' + i + ' = a[' + i + ']; ';
                }
            }
            else {
                code += 'v0;';
                temp += 'v0 = a[0];';
            }
 
            defs = Ext.Array.insert(defs, 0, [code]);
            body = Ext.Array.insert(body, 0, [temp]);
            body = body.join('\n');
 
            //<debug>
            if (debug) {
                body = 'debugger;\n' + body;
            }
            //</debug>
 
            defs.push(
                (me.useEval ? '$=' : 'return') + ' function (values, scope) {',
                body,
                '}'
            );
 
            code = defs.join('\n');
 
            fn = me.useEval ? me.evalFn(code) : (new Function('Ext', code))(Ext);
 
            me.definitions = me.body = null;
 
            return fn;
        },
 
        /**
         * Compiles the specified symbol
         *
         * @param expr
         * @return {String} 
         * @private
         */
        compile: function(expr) {
            var me = this,
                v;
 
            switch (expr.arity) {
                case 'ident':
                    // identifiers are our expression's tokens
                    return me.addToken(expr.value);
 
                case 'literal':
                    v = expr.value;
 
                    // strings need to be escaped before adding them to formula
                    return (typeof v === 'string')
                        ? '"' + String(v).replace(me.escapeRe, '\\$1') + '"'
                        : v;
 
                case 'unary':
                    return me.compileUnary(expr);
 
                case 'binary':
                    return me.compileBinary(expr);
 
                case 'ternary':
                    return me.compileTernary(expr);
 
                case 'formatter':
                    return me.compileFormatter(expr);
 
            }
 
            return this.syntaxError(expr.at, 'Compile error! Unknown symbol');
        },
 
        /**
         * Compiles unary symbol
         *
         * @param expr
         * @return {String} 
         * @private
         */
        compileUnary: function(expr) {
            var v = expr.value,
                op = expr.operand;
 
            if (=== '!' || v === '-' || v === '+') {
                return v + '(' + this.compile(op) + ')';
            }
            else if (=== '@') {
                // @ should be used to prefix global identifiers and nothing else
                if (!op.isIdent) {
                    return this.syntaxError(expr.at, 'Compile error! Unexpected symbol');
                }
 
                return op.value;
            }
 
            return '';
        },
 
        /**
         * Compiles binary symbol
         *
         * @param expr
         * @return {String} 
         * @private
         */
        compileBinary: function(expr) {
            return '(' + this.compile(expr.lhs) + ' ' + expr.value + ' ' + this.compile(expr.rhs) +
                   ')';
        },
 
        /**
         * Compiles ternary symbol
         *
         * @param expr
         * @return {String} 
         * @private
         */
        compileTernary: function(expr) {
            return '(' + this.compile(expr.condition) + ' ? ' + this.compile(expr.tv) + ' : ' +
                   this.compile(expr.fv) + ')';
        },
 
        /**
         * Compiles formatter symbol
         *
         * @param expr
         * @return {String} 
         * @private
         */
        compileFormatter: function(expr) {
            var me = this,
                fmt = expr.fmt,
                length = fmt.length,
                body = [
                    'var ret;'
                ],
                i;
 
            if (fmt.length) {
                body.push('ret = ' + me.compileFormatFn(fmt[0], me.compile(expr.operand)) + ';');
 
                for (= 1; i < length; i++) {
                    body.push('ret = ' + me.compileFormatFn(fmt[i], 'ret') + ';');
                }
            }
 
            body.push('return ret;');
 
            return me.addFn(body.join('\n'));
        },
 
        /**
         * Compiles a single format symbol using `value` as the first argument
         *
         * @param expr
         * @param value
         * @return {String} 
         * @private
         */
        compileFormatFn: function(expr, value) {
            var fmt,
                args = [],
                code = '',
                length, i;
 
            if (expr.isIdent) {
                // the function has no arguments
                fmt = expr.value;
            }
            else if (expr.isInvoke) {
                fmt = expr.operand.value;
                args = expr.args;
            }
 
            if (fmt.substring(0, 5) === 'this.') {
                fmt = 'me.' + fmt.substring(5);
            }
            else {
                if (!(fmt in Ext.util.Format)) {
                    return this.syntaxError(expr.at, 'Compile error! Invalid format specified "' +
                                            fmt + '"');
                }
 
                fmt = 'fm.' + fmt;
            }
 
            code += value;
            length = args.length;
 
            for (= 0; i < length; i++) {
                code += '' + this.compile(args[i]);
            }
 
            return fmt + '(' + code + ')';
        },
 
        /**
         * Adds a new function to the final compiled function
         * @param body
         * @return {string} Name of the function
         * @private
         */
        addFn: function(body) {
            var defs = this.definitions,
                name = 'f' + defs.length;
 
            defs.push(
                'function ' + name + '() {',
                body,
                '}'
            );
 
            return name + '()';
        },
 
        /**
         * Evaluates a function
         * @param $
         * @return {Function} 
         * @private
         */
        evalFn: function($) {
            eval($);
 
            return $;
        },
 
        /**
         * Adds the specified expression token to the internal tokens
         * @param token
         * @return {string} Name of the variable assigned for this token in the compiled function
         * @private
         */
        addToken: function(token) {
            var tokensMap = this.tokensMap,
                tokens = this.tokens,
                pos = 0;
 
            // token can be ignored when this function is called via `compileFormatFn`
            if (tokensMap && tokens) {
                if (token in tokensMap) {
                    pos = tokensMap[token];
                }
                else {
                    tokensMap[token] = pos = tokens.length;
                    tokens.push(token);
                }
            }
 
            return 'v' + pos;
        }
    }
});