/**
 * This class holds the parsed text for a bind template. The syntax is that of a normal
 * `Ext.Template` except that substitution tokens can contain dots to reference property
 * names.
 *
 * The template is parsed and stored in a representation like this:
 *
 *      me.text = 'Hey {foo.bar}! Test {bar} and {foo.bar} with {abc} over {bar:number}'
 *
 *      me.tokens = [ 'foo.bar', 'bar', 'abc' ]
 *
 *      me.buffer = [           me.slots = [
 *          'Hey ',                 undefined,
 *          undefined,              { token: 'foo.bar', pos: 0 },
 *          '! Test ',              undefined,
 *          undefined,              { token: 'bar', pos: 1 },
 *          ' and ',                undefined,
 *          undefined,              { token: 'foo.bar', pos: 0 },
 *          ' with ',               undefined,
 *          undefined,              { token: 'abc', pos: 2 },
 *          ' over ',               undefined,
 *          undefined               { token: 'bar', fmt: 'number', pos: 1 }
 *      ]                       ]
 *
 * @private
 * @since 5.0.0
 */
Ext.define('Ext.app.bind.Template', {
    requires: [
        'Ext.util.Format'
    ],

    numberRe: /^(?:\d+(?:\.\d*)?)$/,

    stringRe: /^(?:["][^"]*["])$/,

    /**
     * @property {RegExp} tokenRe
     * Regular expression used to extract tokens.
     *
     * Finds the following expressions within a format string
     *
     *                     {AND?}
     *                     /    \
     *                   /        \
     *                 /            \
     *               /                \
     *            OR                   AND?
     *           /  \                  / \
     *          /    \                /   \
     *         /      \              /     \
     *    (\d+)  ([a-z_][\w\-\.]*)  /       \
     *     index       name       /         \
     *                           /           \
     *                          /             \
     *                   :([a-z_\.]*)    (?:\(([^\)]*?)?\))?
     *                      formatFn           args
     *
     * Numeric index or (name followed by optional formatting function and args)
     * @private
     */
    tokenRe: /\{[!]?(?:(?:(\d+)|([a-z_][\w\-\.]*))(?::([a-z_\.]+)(?:\(([^\)]*?)?\))?)?)\}/gi,

    formatRe: /^([a-z_]+)(?:\(([^\)]*?)?\))?$/i,

    /**
     * @property {String[]} buffer
     * Initially this is just the array of string fragments with `null` between each
     * to hold the place of a substitution token. On first use these slots are filled
     * with the token's value and this array is joined to form the output.
     * @private
     */
    buffer: null,

    /**
     * @property {Object[]} slots
     * The elements of this array line up with those of `buffer`. This array holds
     * the parsed information for the substitution token that fills a given slot in
     * the generated string. Indices that correspond to literal text are `null`.
     *
     * Consider the following substitution token:
     *
     *      {foo:this.fmt(2,4)}
     *
     * The object in this array has the following properties to describe this token:
     *
     *   * `fmt` The name of the formatting function ("fmt") or `null` if none.
     *   * `index` The numeric index if this is not a named substitution or `null`.
     *   * `not` True if the token has a logical not ("!") at the front.
     *   * `token` The name of the token ("foo") if not an `index`.
     *   * `pos` The position of this token in the `tokens` array.
     *   * `scope` A reference to the object on which the `fmt` method exists. This
     *    will be `Ext.util.Format` if no "this." is present or `null` if it is (or
     *    if there is no `fmt`). In the above example, this is `null` to indicate the
     *    scope is unknown.
     *   * `args` An array of arguments to `fmt` if the arguments are simple enough
     *    to parse directly. Otherwise this is `null` and `fn` is used.
     *   * `fn` A generated function to use to evaluate the arguments to the `fmt`. In
     *    rare cases these arguments can reference global variables so the expression
     *    must be evaluated on each call.
     *   * `format` The method to call to perform the format. This method accepts the
     *    scope (in case `scope` is unknown) and the value. This function is `null` if
     *    there is no `fmt`.
     *
     * @private
     */
    slots: null,

    /**
     * @property {String[]} tokens
     * The distinct set of tokens used in the template excluding formatting. This is
     * used to ensure that only one bind is performed per unique token. This array is
     * passed to {@link Ext.app.ViewModel#bind} to perform a "multi-bind". The result
     * is an array of values corresponding these tokens. Each entry in `slots` then
     * knows its `pos` in this array from which to pick up its value, apply formats
     * and place in `buffer`.
     * @private
     */
    tokens: null,

    /**
     * @param {String} text The text of the template.
     */
    constructor: function (text) {
        var me = this,
            initters = me._initters,
            name;

        me.text = text;

        for (name in initters) {
            me[name] = initters[name];
        }
    },

    /**
     * @property {Object} _initters
     * Each of the methods contained on this object are placed in new instances to lazily
     * parse the template text.
     * @private
     * @since 5.0.0
     */
    _initters: {
        apply: function (values, scope) {
            return this.parse().apply(values, scope);
        },
        getTokens: function () {
            return this.parse().getTokens();
        }
    },

    /**
     * Applies this template to the given `values`. The `values` must correspond to the
     * `tokens` returned by `getTokens`.
     *
     * @param {Array} values The values of the `tokens`.
     * @param {Object} scope The object instance to use for "this." formatter calls in the
     * template.
     * @return {String}
     * @since 5.0.0
     */
    apply: function (values, scope) {
        var me = this,
            slots = me.slots,
            buffer = me.buffer,
            length = slots.length,  // === buffer.length
            i, slot, value;

        for (i = 0; i < length; ++i) {
            slot = slots[i];
            if (slot) {
                if ((value = values[slot.pos]) == null) {
                    // map (value === null || value === undefined) to '':
                    value = '';
                }
                if (slot.not) {
                    value = !value;
                }
                if (slot.format) {
                    value = slot.format(value, scope);
                }
                buffer[i] = value;
            }
        }

        return buffer.join('');
    },

    /**
     * Returns the distinct set of binding tokens for this template.
     * @return {String[]} The `tokens` for this template.
     */
    getTokens: function () {
        return this.tokens;
    },

    /**
     * Parses the template text into `buffer`, `slots` and `tokens`. This method is called
     * automatically when the template is first used.
     * @return {Ext.app.bind.Template} this
     * @private
     */
    parse: function () {
        // NOTE: The particulars of what is stored here, while private, are likely to be
        // important to Sencha Architect so changes need to be coordinated.
        var me = this,
            text = me.text,
            buffer = [],
            slots = [],
            tokens = [],
            tokenMap = {},
            last = 0,
            tokenRe = me.tokenRe,
            pos = 0,
            fmt, i, length, match, s, slot, token;

        // Remove the initters so that we don't get called here again.
        for (i in me._initters) {
            delete me[i];
        }

        me.buffer = buffer;

        me.slots = slots;

        me.tokens = tokens;

        // text = 'Hello {foo:this.fmt(2,4)} World {bar} - {1}'
        while ((match = tokenRe.exec(text))) {
            //   0                      1          2         3           4         index
            // [ '{foo:this.fmt(2,4)}', undefined, 'foo',    'this.fmt', '2,4']        6
            // [ '{bar}',               undefined, 'bar',     undefined,  undefined]  32
            // [ '{1}',                 '1',       undefined, undefined,  undefined]  40
            length = match.index - last;
            if (length) {
                buffer[pos++] = text.substring(last, last + length);
                last += length;
            }
            last += (s = match[0]).length;

            slot = {
                fmt: (fmt = match[3] || null),
                index: match[1] ? parseInt(match[1], 10) : null,
                not: s.charAt(1) === '!',
                token: match[2] || null
            };

            token = slot.token || String(slot.index);
            if (token in tokenMap) {
                slot.pos = tokenMap[token];
            } else {
                tokenMap[token] = slot.pos = tokens.length;
                tokens.push(token);
            }

            if (fmt) {
                if (fmt.substring(0,5) === 'this.') {
                    slot.fmt = fmt.substring(5);
                } else {
                    //<debug>
                    if (!(fmt in Ext.util.Format)) {
                        Ext.Error.raise('Invalid format specified: "' + fmt + '"');
                    }
                    //</debug>
                    slot.scope = Ext.util.Format;
                }

                me.parseArgs(match[4], slot);
            }

            slots[pos++] = slot;
        }

        if (last < text.length) {
            buffer[pos++] = text.substring(last);
        }

        return me;
    },

    parseArgs: function (argsString, slot) {
        var me = this,
            numberRe = me.numberRe,
            stringRe = me.stringRe,
            arg, args, i, length;

        if (!argsString) {
            args = [];
        } else if (argsString.indexOf(',') < 0) {
            args = [argsString];
        } else {
            args = argsString.split(',');
        }

        slot = slot || {};
        length = args.length;
        slot.args = args;

        for (i = 0; i < length; ++i) {
            arg = args[i];
            if (arg === 'true') {
                args[i] = true;
            } else if (arg === 'false') {
                args[i] = false;
            } else if (arg === 'null') {
                args[i] = null;
            } else if (numberRe.test(arg)) {
                args[i] = parseFloat(arg);
            } else if (stringRe.test(arg)) {
                args[i] = arg.substring(1, arg.length - 1);
            } else {
                slot.fn = Ext.functionFactory('return ['+  argsString +'];');
                slot.format = me._formatEval;
                break;
            }
        }

        if (!slot.format) {
            // make room for the value at index 0
            args.unshift(0);
            slot.format = me._formatArgs;
        }

        return slot;
    },

    /**
     * This method parses token formats and returns an object with a `format` method that
     * can format values accordingly.
     * @param {String} fmt The format suffix of a template token. For example, in the
     * token "{foo:round(2)}" the format is "round(2)".
     * @return {Object} An object with a `format` method to format values.
     * @private
     * @since 5.0.0
     */
    parseFormat: function (fmt) {
        var me = this,
            match = me.formatRe.exec(fmt),
            slot = {
                fmt: fmt,
                scope: Ext.util.Format
            },
            args;

        //<debug>
        if (!match) {
            Ext.Error.raise('Invalid format syntax: "' + slot + '"');
        }
        //</debug>

        args = match[2];
        if (args) {
            slot.fmt = match[1];
            me.parseArgs(args, slot);
        } else {
            slot.args = [0]; // for the value
            slot.format = me._formatArgs;
        }

        return slot;
    },

    /**
     * This method is placed on an entry in `slots` as the `format` method when that entry
     * has `args` that could be parsed from the template.
     * @param {Object} value The value of the token.
     * @param {Object} [scope] The object instance to use for "this." formatter calls in the
     * template.
     * @return {String} The formatted result to place in `buffer`.
     * @private
     * @since 5.0.0
     */
    _formatArgs: function (value, scope) {
        // NOTE: our "this" pointer is the object in the "slots" array!
        scope = this.scope || scope;
        this.args[0] = value; // index 0 is reserved for the value
        return scope[this.fmt].apply(scope, this.args);
    },

    /**
     * This method is placed on an entry in `slots` as the `format` method when that entry
     * does not have a parsed `args` array.
     * @param {Object} value The value of the token.
     * @param {Object} [scope] The object instance to use for "this." formatter calls in the
     * template.
     * @return {String} The formatted result to place in `buffer`.
     * @private
     * @since 5.0.0
     */
    _formatEval: function (value, scope) {
        // NOTE: our "this" pointer is the object in the "slots" array!
        var args = this.fn(); // invoke to get the args array
        args.unshift(value); // inject the value at the front
        scope = this.scope || scope;
        return scope[this.fmt].apply(scope, args);
    }
});
