/**
 * This class accepts an object that serves as a template for creating new objects. Like
 * other templates (`Ext.XTemplate`) this creation step requires a context object to give
 * the template its values.
 *
 * For example:
 *
 *      var tpl = new Ext.util.ObjectTemplate({
 *          property: 'Hello {name}',
 *          data: {
 *              value: '{age}'
 *          }
 *      });
 *
 *      var obj = tpl.apply({
 *          name: 'Bill',
 *          age: 42
 *      });
 *
 *      // obj = {
 *      //     property: 'Hello Bill',
 *      //     data: {
 *      //         value: 42
 *      //     }
 *      // }
 *
 * @since 5.0.0
 */
Ext.define('Ext.util.ObjectTemplate', {
    requires: [
        'Ext.XTemplate'
    ],

    isObjectTemplate: true,

    excludeProperties: {},

    valueRe: /^[{][a-z\.]+[}]$/i,

    statics: {
        /**
         * Creates an `ObjectTemplate` given a config object or instance.
         * @param {Object/Ext.util.ObjectTemplate} template The template object.
         * @param {Object} [options]
         * @return {Ext.util.ObjectTemplate}
         * @since 5.0.0
         */
        create: function (template, options) {
            //<debug>
            if (!Ext.isObject(template)) {
                Ext.Error.raise('The template is not an Object');
            }
            //</debug>

            return template.isObjectTemplate ? template
                                : new Ext.util.ObjectTemplate(template, options);
        }
    },

    /**
     * Constructs the `ObjectTemplate`. The actual compilation of the object to a ready to
     * apply form happens on the first call to `apply`.
     * @param {Object} template
     * @param {Object} [options]
     * @since 5.0.0
     */
    constructor: function (template, options) {
        Ext.apply(this, options);

        this.template = template;
    },

    /**
     * Applies the given `context` object to this template and returns a new object with
     * the appropriate pieces replaced.
     * @param {Object} context The data used to populate the template.
     * @return {Object}
     * @since 5.0.0
     */
    apply: function (context) {
        var me = this;

        delete me.apply;

        me.apply = me.compile(me.template);

        return me.apply(context);
    },

    privates: {
        /**
         * Compiles the  given template into an `apply` method that is ready to run. This
         * method is used recursively to process object properties and array elements.
         * @param {Mixed} template
         * @return {Function}
         * @since 5.0.0
         */
        compile: function (template) {
            var me = this,
                exclude = me.excludeProperties,
                compiled, i, len, fn;

            // TODO: loops over array or objects

            if (Ext.isString(template)) {
                if (template.indexOf('{') < 0) {
                    fn = function () {
                        return template;
                    };
                } else if (me.valueRe.test(template)) {
                    template = template.substring(1, template.length - 1).split('.');

                    fn = function (context) {
                        for (var v = context, i = 0; v && i < template.length; ++i) {
                            v = v[template[i]];
                        }
                        return v;
                    };
                } else {
                    template = new Ext.XTemplate(template);

                    fn = function (context) {
                        return template.apply(context);
                    };
                }
            } else if (!template || Ext.isPrimitive(template) || Ext.isFunction(template)) {
                fn = function () {
                    return template;
                };
            } else if (template instanceof Array) {
                compiled = [];

                for (i = 0, len = template.length; i < len; ++i) {
                    compiled[i] = me.compile(template[i]);
                }

                fn = function (context) {
                    var ret = [],
                        i;

                    for (i = 0; i < len; ++i) {
                        ret[i] = compiled[i](context);
                    }

                    return ret;
                };
            } else {
                compiled = {};

                for (i in template) {
                    if (!exclude[i]) {
                        compiled[i] = me.compile(template[i]);
                    }
                }

                fn = function (context) {
                    var ret = {},
                        i, v;

                    for (i in template) {
                        v = exclude[i] ? template[i] : compiled[i](context);
                        if (v !== undefined) {
                            ret[i] = v;
                        }
                    }

                    return ret;
                };
            }

            return fn;
        }
    }
});
