/**
 * 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;
            }
        },
        '===':  40,
        '!==':  40,
        '==':   40,
        '!=':   40,
        '<':    40,
        '<=':   40,
        '>':    40,
        '>=':   40
    },
 
    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 parans
                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 fn;
 
        //<debug>
        try {
        //</debug>
            fn = this.parseSlot({
                arity: 'formatter',
                fmt: this.parseFmt(),
                operand: {
                    arity: 'ident',
                    value: 'dummy'
                }
            });
            this.expect('(end)');
        //<debug>
        } catch (e) {
            Ext.raise('Invalid format expression: "' + this.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;
        }
    }
 
});