(function() {
    var Ext = window.Ext4 || window.Ext;

    /**
     * A generic ModelFactory that will delegate calls to Rally.data.ModelFactory.getModels(s) to
     * the correct model factory. Model factories must register themselves to this class using the
     * ::registerType(s) methods.
     *
     * This class is meant to abstract where models come from so all the user has to do is get
     * models by standardized model names.
     *
     * See [Data Models](#!/guide/data_models) for more information on working with models.
     *
     *     Rally.data.ModelFactory.getModel({
     *         type: 'Defect',
     *         context: {
     *             workspace: '/workspace/12345'
     *         }
     *         success: function(model) {
     *
     *         }
     *     });
     */
    Ext.define('Rally.data.ModelFactory', {
        singleton: true,

        uses: [
            'Rally.data.wsapi.ModelFactory'
        ],

        /**
         * @property {Object} types
         * A hash object of types. This is where factories are registered based on a type string
         */

        constructor: function(config) {
            Ext.apply(this, config || {});

            this.types = {};
        },

        /**
         * Register a model factory to create a model for a given model type, exp: 'userstory'
         * @param {String} type The type/key string representing a model. Ex: 'userstory'
         * @param {Class} factory A factory that implements stuff to get models given a config
         */
        registerType: function (type, factory) {
            this.types[this._getCompatibleType(type)] = factory;
        },

        /**
         * Register a model factory to create models for an array of model types
         * @param {Array} types An array of type strings to register
         * @param {Class} factory
         */
        registerTypes: function (types, factory) {
            _.each(types, function (type) {
                this.registerType(type, factory);
            }, this);
        },

        clearTypes: function () {
            this.types = {};
        },

        getFactory: function (type) {
            return this.types[this._getCompatibleType(type)];
        },

        /**
         * Get a model
         * @param {Object} options
         * @param {String} options.type the type to build the model for, e.g., defect
         * @param {Object} [options.context] An object containing the scope (workspace) where the model is defined
         * If not specified it will default to the workspace defined by Rally.env.Environment#getContext.
         * @param {String/Number} [options.wsapiVersion] The WSAPI version to use when building the model
         * If not specified it will default to the version defined by Rally.env.Environment#getServer.
         * @param {Function} [options.success] success callback, given the model
         * @param {Rally.data.Model} options.success.model The matching model object
         * @param {Object} [options.scope=this] an object to call the success callback against
         * @param {Function} [options.failure] if we can't build
         * @return {Deft.Promise} promise The promise to be resolved or rejected by the delegated model factory. The alternative is using success and failure callbacks
         *
         *     Rally.data.ModelFactory.getModel({
         *         type: 'Defect',
         *         context: {
         *             workspace: '/workspace/12345'
         *         }
         *         success: function(model) {
         *
         *         }
         *     });
         */
        getModel: function(options) {
            // <debug>
            if (!options.type || !this.getFactory(options.type)) {
                Ext.Error.raise('Could not find registered factory for type:  ' + options.type);
            }
            // </debug>
            var factory = this.getFactory(options.type);
            return factory.getModel.call(factory, options);
        },

        /**
         * Get multiple models.
         * @param options
         * @param {String[]} options.types An array of types to build the model for, e.g., [defect,task]
         * @param {Object} options.context An object containing the scope (workspace) where the model is defined
         * If not specified it will default to the workspace defined by Rally.env.Environment#getContext.
         * @param {String/Number} options.wsapiVersion The WSAPI version to use when building the model
         * If not specified it will default to the version defined by Rally.env.Environment#getServer.
         * @param {Function} options.success success callback, returns an object with property names set to the name of the model: ie models.defect is the defect model
         * @param {Object} options.success.models Matching models
         * @param {Object} [options.scope=window] an object to call the success callback against (optional)
         * @param {Function} options.failure called if an error occurs
         * @return {Deft.Promise} promise The promise to be resolved or rejected by the delegated model factories. The alternative is using success and failure callbacks
         *
         *     Rally.data.ModelFactory.getModels({
         *         types: ['Defect', 'UserStory'],
         *         context: {
         *             workspace: '/workspace/12345'
         *         }
         *         success: function(models) {
         *             var defectModel = models.Defect;
         *         }
         *     });
         */
        getModels: function(options) {

            var me = Rally.data.ModelFactory,
                successFn = options.success,
                failureFn = options.failure,
                factories = {},
                promises = [];

            // remove success and failure callbacks from the config to force promises
            options.success = Ext.emptyFn;
            options.failure = Ext.emptyFn;

            options.types = this._ensureTypesAreDefined(options.types);

            _.each(options.types, function(type) {
                // <debug>
                if (!me.getFactory(me._getCompatibleType(type))) {
                    Ext.Error.raise('Factory is not registered for type: ' + type);
                }
                // </debug>
                var factory = me.getFactory(me._getCompatibleType(type)).$className;

                // batch up types to the correct factory
                if (factories[factory]) {
                    factories[factory].push(type);
                } else {
                    factories[factory] = [type];
                }
            }, me);

            _.each(factories, function (types, className) {
                var factory = Ext.ClassManager.get(className);
                // create a new options object and override the types
                var promise = factory.getModels(Ext.applyIf({ types: types }, options));
                // <debug>
                if (promise === undefined) {
                    Ext.Error.raise(className + '.getModels should return a value or a promise');
                }
                // </debug>
                promises.push(promise);
            });

            // return a new promise that will wait for all promises to resolve
            // Also call any passed callbacks
            var deferred = new Deft.Deferred();
            Deft.Promise.all(promises).then(function(values) {
                if (successFn) {
                    // Prevent unpredictable sync/async paths
                    Ext.callback(successFn, options.scope, [me._flattenPromiseValues(values)], 1);
                }
                deferred.resolve(me._flattenPromiseValues(values));
            }).otherwise(function (err) {
                if (failureFn) {
                    // Prevent unpredictable sync/async paths
                    Ext.callback(failureFn, options.scope, [err], 1);
                }
                deferred.reject(err);
            });

            return deferred.promise;
        },

        _ensureTypesAreDefined: function(types) {
            // <debug>
            if (!types) {
                Ext.Error.raise('getModels requires "types" to be defined');
            }
            // </debug>

            types = _.isArray(types) ? types : [types];

            // <debug>
            if (!types.length) {
                Ext.Error.raise('getModels requires "types" to be defined');
            }
            // </debug>

            return types;
        },

        _getCompatibleType: function (type) {
            if(type) {
                return type.toLowerCase().replace(/\s/g, '').split('/')[0];
            }
        },

        _flattenPromiseValues: function (values) {
            var flattenedValue = {};
            _.each(values, function(value) {
                Ext.apply(flattenedValue, value);
            });
            return flattenedValue;
        }
    });
})();