/**
 * Represents an HTML fragment template. Templates may be {@link #compile precompiled} for greater performance.
 *
 * An instance of this class may be created by passing to the constructor either a single argument, or multiple
 * arguments:
 *
 * # Single argument: String/Array
 *
 * The single argument may be either a String or an Array:
 *
 * - String:
 *
 *       var t = new Ext.Template("<div>Hello {0}.</div>");
 *       t.{@link #append}('some-element', ['foo']);
 *
 * - Array:
 *
 *   An Array will be combined with `join('')`.
 *
 *       var t = new Ext.Template([
 *           '<div name="{id}">',
 *               '<span class="{cls}">{name:trim} {value:ellipsis(10)}</span>',
 *           '</div>',
 *       ]);
 *       t.{@link #compile}();
 *       t.{@link #append}('some-element', {id: 'myid', cls: 'myclass', name: 'foo', value: 'bar'});
 *
 * # Multiple arguments: String, Object, Array, ...
 *
 * Multiple arguments will be combined with `join('')`.
 *
 *     var t = new Ext.Template(
 *         '<div name="{id}">',
 *             '<span class="{cls}">{name} {value}</span>',
 *         '</div>',
 *         // a configuration object:
 *         {
 *             compiled: true,      // {@link #compile} immediately
 *         }
 *     );
 *
 * # Notes
 *
 * - For a list of available format functions, see {@link Ext.util.Format}.
 * - `disableFormats` reduces `{@link #apply}` time when no formatting is required.
 */
Ext.define('Ext.Template', {
// @define Ext.String.format
// @define Ext.util.Format.format
    
    requires: [
        //'Ext.dom.Helper',
        'Ext.util.Format'
    ],

    inheritableStatics: {
        /**
         * Creates a template from the passed element's value (_display:none_ textarea, preferred) or innerHTML.
         * @param {String/HTMLElement} el A DOM element or its id
         * @param {Object} config (optional) Config object
         * @return {Ext.Template} The created template
         * @static
         * @inheritable
         */
        from: function(el, config) {
            el = Ext.getDom(el);
            return new this(el.value || el.innerHTML, config || '');
        }
    },

    // 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,

    /* End Definitions */

    /**
     * Creates new template.
     * 
     * @param {String...} html List of strings to be concatenated into template.
     * Alternatively an array of strings can be given, but then no config object may be passed.
     * @param {Object} config (optional) Config object
     */
    constructor: function(html) {
        var me = this,
            args = arguments,
            buffer = [],
            i,
            length = args.length,
            value;

        me.initialConfig = {};
        
        // Allow an array to be passed here so we can
        // pass an array of strings and an object
        // at the end
        if (length === 1 && Ext.isArray(html)) {
            args = html;
            length = args.length;
        }

        if (length > 1) {
            for (i = 0; i < length; i++) {
                value = args[i];
                if (typeof value === 'object') {
                    Ext.apply(me.initialConfig, value);
                    Ext.apply(me, value);
                } else {
                    buffer.push(value);
                }
            }
        } else {
            buffer.push(html);
        }

        // @private
        me.html = buffer.join('');
    },

    /**
     * @property {Boolean} isTemplate
     * `true` in this class to identify an object as an instantiated Template, or subclass thereof.
     */
    isTemplate: true,

    /**
     * @cfg {Boolean} compiled
     * True to immediately compile the template. Defaults to false.
     */

    /**
     * @cfg {Boolean} disableFormats
     * True to disable format functions in the template. If the template doesn't contain
     * format functions, setting disableFormats to true will reduce apply time. Defaults to false.
     */
    disableFormats: false,

    /**
     * @property {RegExp} re
     * 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,

    /**
     * Returns an HTML fragment of this template with the specified values applied.
     *
     * @param {Object/Array} values The template values. Can be an array if your params are numeric:
     *
     *     var tpl = new Ext.Template('Name: {0}, Age: {1}');
     *     tpl.apply(['John', 25]);
     *
     * or an object:
     *
     *     var tpl = new Ext.Template('Name: {name}, Age: {age}');
     *     tpl.apply({name: 'John', age: 25});
     *
     * @return {String} The HTML fragment
     */
    apply: function(values) {
        var me = this;

        if (me.compiled) {
            if (!me.fn) {
                me.compile();
            }
            return me.fn(values).join('');
        }

        return me.evaluate(values);
    },

    // Private
    // Do not create the substitution closure on every apply call
    evaluate: function(values) {
        var me = this,
            useFormat = !me.disableFormats,
            fm = Ext.util.Format,
            tpl = me;

        function fn(match, index, name, formatFn, args) {
            // Calculate the correct name extracted from the {}
            // Certain browser pass unmatched parameters as undefined, some as an empty string.
            if (name == null || name === '') {
                name = index;
            }
            if (formatFn && useFormat) {
                if (args) {
                    args = [values[name]].concat(Ext.functionFactory('return ['+ args +'];')());
                } else {
                    args = [values[name]];
                }

                // Caller used '{0:this.bold}'. Create a call to tpl member function
                if (formatFn.substr(0, 5) === "this.") {
                    return tpl[formatFn.substr(5)].apply(tpl, args);
                }
                // Caller used '{0:number("0.00")}'. Create a call to Ext.util.Format function
                else if (fm[formatFn]) {
                    return fm[formatFn].apply(fm, args);
                }
                // Caller used '{0:someRandomText}'. We must return it unchanged
                else {
                    return match;
                }
            }
            else {
                return values[name] !== undefined ? values[name] : "";
            }
        }

        return me.html.replace(me.tokenRe, fn);
    },

    /**
     * Appends the result of this template to the provided output array.
     * @param {Object/Array} values The template values. See {@link #apply}.
     * @param {Array} out The array to which output is pushed.
     * @return {Array} The given out array.
     */
    applyOut: function(values, out) {
        var me = this;

        if (me.compiled) {
            if (!me.fn) {
                me.compile();
            }
            out.push.apply(out, me.fn(values));
        } else {
            out.push(me.apply(values));
        }

        return out;
    },

    /**
     * @method applyTemplate
     * @member Ext.Template
     * Alias for {@link #apply}.
     * @inheritdoc Ext.Template#apply
     */
    applyTemplate: function () {
        return this.apply.apply(this, arguments);
    },

    /**
     * Sets the HTML used as the template and optionally compiles it.
     * @param {String} html
     * @param {Boolean} compile (optional) True to compile the template.
     * @return {Ext.Template} this
     */
    set: function(html, compile) {
        var me = this;
        me.html = html;
        me.compiled = !!compile;
        me.fn = null;
        return me;
    },

    compileARe: /\\/g,
    compileBRe: /(\r\n|\n)/g,
    compileCRe: /'/g,

    /**
     * Compiles the template into an internal function, eliminating the RegEx overhead.
     * @return {Ext.Template} this
     */
    compile: function() {
        var me = this,
            code;

        code = me.html.replace(me.compileARe, '\\\\').replace(me.compileBRe, '\\n').
                       replace(me.compileCRe, "\\'").replace(me.tokenRe, me.regexReplaceFn.bind(me));
        code = (this.disableFormats !== true ? 'var fm=Ext.util.Format;' : '') +
                (me.useEval ? '$=' : 'return') +
                " function(v){return ['" + code + "'];};";

        me.fn  = me.useEval ? me.evalCompiled(code) : (new Function('Ext', code))(Ext); // jshint ignore:line
        me.compiled = true;
        return me;
    },

    // @private
    evalCompiled: function($) {

        // We have to use eval to realize the code block and capture the inner func we also
        // don't want a deep scope chain. We only do this in Firefox and it is also unhappy
        // with eval containing a return statement, so instead we assign to "$" and return
        // that. Because we use "eval", we are automatically sandboxed properly.
        eval($); // jshint ignore:line
        return $;
    },

    regexReplaceFn: function (match, index, name, formatFn, args) {
        // Calculate the correct expression to use to index into the values object/array
        // index may be a numeric string, or a quoted alphanumeric string.
        // Certain browser pass unmatched parameters as undefined, some as an empty string.
        if (index == null || index === '') {
            index = '"' + name + '"';
        }
        // If we are being used as a formatter for Ext.String.format, we must skip the string itself in the argument list.
        // Doing this enables String.format to omit the Array slice call.
        else if (this.stringFormat) {
            index = parseInt(index) + 1;
        }
        if (formatFn && this.disableFormats !== true) {
            args = args ? ',' + args: "";

            // Caller used '{0:this.bold}'. Create a call to member function
            if (formatFn.substr(0, 5) === "this.") {
                formatFn = formatFn + '(';
            }
            // Caller used '{0:number("0.00")}'. Create a call to Ext.util.Format function
            else if (Ext.util.Format[formatFn]) {
                formatFn = "fm." + formatFn + '(';
            }
            // Caller used '{0:someRandomText}'. We must pass it through unchanged
            else {
                return match;
            }
            return "'," + formatFn + "v[" + index + "]" + args + "),'";
        }
        else {
            return "',v[" + index + "] == undefined ? '' : v[" + index + "],'";
        }
    },

    /**
     * Applies the supplied values to the template and inserts the new node(s) as the first child of el.
     *
     * @param {String/HTMLElement/Ext.dom.Element} el The context element
     * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
     * @param {Boolean} returnElement (optional) true to return a Ext.Element.
     * @return {HTMLElement/Ext.dom.Element} The new node or Element
     */
    insertFirst: function(el, values, returnElement) {
        return this.doInsert('afterBegin', el, values, returnElement);
    },

    /**
     * Applies the supplied values to the template and inserts the new node(s) before el.
     *
     * @param {String/HTMLElement/Ext.dom.Element} el The context element
     * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
     * @param {Boolean} returnElement (optional) true to return a Ext.Element.
     * @return {HTMLElement/Ext.dom.Element} The new node or Element
     */
    insertBefore: function(el, values, returnElement) {
        return this.doInsert('beforeBegin', el, values, returnElement);
    },

    /**
     * Applies the supplied values to the template and inserts the new node(s) after el.
     *
     * @param {String/HTMLElement/Ext.dom.Element} el The context element
     * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
     * @param {Boolean} returnElement (optional) true to return a Ext.Element.
     * @return {HTMLElement/Ext.dom.Element} The new node or Element
     */
    insertAfter: function(el, values, returnElement) {
        return this.doInsert('afterEnd', el, values, returnElement);
    },

    /**
     * Applies the supplied `values` to the template and appends the new node(s) to the specified `el`.
     *
     * For example usage see {@link Ext.Template Ext.Template class docs}.
     *
     * @param {String/HTMLElement/Ext.dom.Element} el The context element
     * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
     * @param {Boolean} returnElement (optional) true to return an Ext.Element.
     * @return {HTMLElement/Ext.dom.Element} The new node or Element
     */
    append: function(el, values, returnElement) {
        return this.doInsert('beforeEnd', el, values, returnElement);
    },

    doInsert: function(where, el, values, returnElement) {
        var newNode = Ext.DomHelper.insertHtml(where, Ext.getDom(el), this.apply(values));
        return returnElement ? Ext.get(newNode) : newNode;
    },

    /**
     * Applies the supplied values to the template and overwrites the content of el with the new node(s).
     *
     * @param {String/HTMLElement/Ext.dom.Element} el The context element
     * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
     * @param {Boolean} returnElement (optional) true to return a Ext.Element.
     * @return {HTMLElement/Ext.dom.Element} The new node or Element
     */
    overwrite: function(el, values, returnElement) {
        var newNode = Ext.DomHelper.overwrite(Ext.getDom(el), this.apply(values));
        return returnElement ? Ext.get(newNode) : newNode;
    }
}, function(Template){
    
    var formatRe = /\{\d+\}/,
        generateFormatFn = function(format) {
            // Generate a function which substitutes value tokens
            if (formatRe.test(format)) {
                format = new Template(format, formatTplConfig);
                return function() {
                    return format.apply(arguments);
                };
            }
            // No value tokens
            else {
                return function() {
                    return format;
                };
            }
        },
        // Flags for the template compile process.
        // stringFormat means that token 0 consumes argument 1 etc.
        // So that String.format does not have to slice the argument list.
        formatTplConfig = { useFormat: false, compiled: true, stringFormat: true },
        formatFns = {};

    /**
     * Alias for {@link Ext.String#format}.
     * @method format
     * @inheritdoc Ext.String#format
     * @member Ext.util.Format
     */
    /**
     * Allows you to define a tokenized string and pass an arbitrary number of arguments to replace the tokens.  Each
     * token must be unique, and must increment in the format {0}, {1}, etc.  Example usage:
     *
     *     var cls = 'my-class',
     *         text = 'Some text';
     *     var s = Ext.String.format('<div class="{0}">{1}</div>', cls, text);
     *     // s now contains the string: '<div class="my-class">Some text</div>'
     *
     * @param {String} string The tokenized string to be formatted.
     * @param {Mixed...} values The values to replace tokens `{0}`, `{1}`, etc in order.
     * @return {String} The formatted string.
     * @member Ext.String
     */
    Ext.String.format = Ext.util.Format.format = function(format) {
        var formatFn = formatFns[format] || (formatFns[format] = generateFormatFn(format));
        return formatFn.apply(this, arguments);
    };
});
