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

    var wsapiMaxPageSize = 200;

    /**
     * A data store which can retrieve data from Rally's WSAPI.
     * See the [Data Stores](#!/guide/data_stores) guide for more information on working with web services.
     *
     *     Ext.create('Rally.data.wsapi.Store', {
     *         model: 'User Story',
     *         autoLoad: true,
     *         listeners: {
     *             load: function(store, data, success) {
     *                 //process data
     *             }
     *         },
     *         fetch: ['Name', 'ScheduleState']
     *     });
     *
     * The store accepts either a string or {@link Rally.data.wsapi.Model} representing the type to retrieve.
     * The store supports the standard Ext store config properties for filtering and sorting.
     *
     *     Ext.create('Rally.data.wsapi.Store', {
     *         model: userStoryModel,
     *         fetch: true,
     *         autoLoad: true,
     *         filters: [
     *             {
     *                 property: 'ScheduleState',
     *
     *                 value: 'Accepted'
     *             }
     *         ],
     *         sorters: [
     *             {
     *                 property: 'FormattedID',
     *                 direction: 'ASC'
     *             }
     *         ]
     *     });
     *
     * Scoping can be controlled via the {@link Rally.data.wsapi.Store#context context} config parameter.
     * This can be an object literal or if running in an app can be obtained via the {@link Rally.app.App#getContext} method.
     *
     *     Ext.create('Rally.data.wsapi.Store', {
     *         model: userStoryModel,
     *         context: {
     *             project: '/project/1234',
     *             projectScopeUp: false,
     *             projectScopeDown: false
     *         },
     *         autoLoad: true
     *     });
     *
     *
     */
    Ext.define('Rally.data.wsapi.Store', {

        // Client Metrics Note: WsapiDataStore is too low level to record its load begins/ends. The problem is
        // client metrics can only reliable keep track of one load per component at a time. WsapiDataStore makes
        // no guarantee that only one load will happen at a time. It's better to measure the component that is using
        // the store. All is not lost, the actual data requests that WsapiDataStore makes *are* measured by client metrics.

        requires: ['Rally.data.ModelFactory', 'Rally.data.Ranker'],
        extend: 'Rally.data.PageableStore',
        alias: ['store.rallywsapidatastore', 'store.rallywsapistore'],
        alternateClassName: ['Rally.data.WsapiDataStore'],
        mixins: {
            messageable: 'Rally.Messageable',
            findRecords: 'Rally.data.WsapiFindRecords',
            recordUpdatable: 'Rally.data.wsapi.RecordUpdatable'
        },

        statics: {
            wsapiMaxPageSize: wsapiMaxPageSize
        },

        /**
         * @cfg {Boolean} autoSync
         * NOT RECOMMENDED with any Rally store. Defaults to false. Editing and pagination will not work when set to true.
         */
        autoSync: false,

        /**
         * @cfg {Number}
         * The number of records to retrieve per page.
         */
        pageSize: wsapiMaxPageSize,

        /**
         * @cfg {String[]}
         * The fields to be retrieved
         */
        fetch: undefined,

        /**
         * @cfg {Boolean} useShallowFetch
         * When enabled, the fields defined by {@link #fetch} will populate the shallowFetch request parameter instead of fetch.
         */
        useShallowFetch: false,

        /**
         * @cfg {Boolean} enablePostGet
         * When enabled, store load will set enablePostGet on the operation sent to the proxy.  Proxy will turn this
         * GET into a POST?_method=GET.  Handy when GET url is too long (resulting in a 413)
         */
        enablePostGet: false,

        /**
         * @private
         * @cfg {Boolean} compact
         * Passed through to the proxy.  Turns on/off WSAPI results compaction.  If undefined, it uses the proxy's default mode.
         */
        compact: undefined,

        /**
         * @cfg {Boolean} includeSchema
         * When enabled, the schema will come down with the results.
         */
        includeSchema: false,

        /**
         * The data scoping to be applied.
         * @cfg {Object} context
         * @cfg {String} context.workspace The ref of the workspace to scope to
         * @cfg {String} context.project The ref of the project to scope to.  Specify null to query the entire specified workspace.
         * @cfg {Boolean} context.projectScopeUp Whether to scope up the project hierarchy
         * @cfg {Boolean} context.projectScopeDown Whether to scope down the project hierarchy
         */
        context: undefined,

        /**
         * The wsapi version to use when automatically retrieving a model before loading.
         * By default the wsapi version of the specified model is used.
         * @cfg {String/Number} wsapiVersion
         */
        wsapiVersion: undefined,

        /**
        * @private
        * @cfg {String}
        * The search string
        * Search can be used on its own or combined with filters.
        * When used on its own, search behaves like the search box in Rally.
        * When used with filters, search is evaluated first and then filters are applied to further narrow the results.
        */
        search: undefined,

        /**
         * @cfg {Boolean}
         * Automatically translate rank-like fields to the appropriate field for
         * this store's model type in fetch and order params.
         * Set to false to maintain the real ultimate power to configure these.
         */
        enableRankFieldParameterAutoMapping: true,

        constructor: function(config) {

            if (config.proxy) {
                this._customProxyInUse = true;
            } else {
                //If a string model type was specified
                //construct a default proxy for now
                if (Ext.isString(config.model)) {
                    config.proxy = Rally.environment.getIoProvider().getProxy({
                        wsapiVersion: config.wsapiVersion
                    });
                }
            }

            this.callParent(arguments);
        },

        /**
         * @inheritdoc
         *
         * @returns Deft.Promise
         */
        load: function(options) {
            this.loading = true;
            options = options || {};

            if (Ext.isFunction(options)) {
                options = {
                    callback: options
                };
            }

            //Load the model first if a string was specified
            if (!this.model && Ext.isString(this.initialConfig.model)) {
                return this._hydrateModelAndLoad(options);
            } else {
                var promise = this._wrapCallbackWithPromise(options);

                this._setFetch(options);
                options.useShallowFetch = this.useShallowFetch;
                options.fetch        = this.fetch;
                options.search       = this.search;
                options.context      = this.context;
                options.requester    = options.requester || this.requester;
                options.enablePostGet = this.enablePostGet;
                options.includeSchema = this.includeSchema;
                options.compact       = this.compact;

                if (this.url) {
                    options.url = this.url;
                }

                this.callParent([options]);

                return promise;
            }
        },

        /**
         * @inheritdoc
         * @returns {Deft.Promise(Ext.data.Batch)}
         */
        sync: function(options) {
            options = options || {};
            var deferred = Ext.create('Deft.Deferred'),
                me = this,
                opts = Ext.apply({}, {
                    callback: function(batch, syncOptions) {
                        Ext.callback(options.callback, options.scope || me, [batch, syncOptions]);
                        if (!batch.hasException) {
                            deferred.resolve(batch);
                        } else {
                            deferred.reject(batch);
                        }
                    }
                }, options);

            this.callParent([opts]);

            return deferred.promise;
        },

        /**
         * Sets the filter without loading or locally filtering data set.
         *
         * @param filter {Object|Object[]} Filters to set.
         */
        setFilter: function(filter) {
            this.clearFilter(true);
            _.each(this.decodeFilters(Ext.Array.from(filter)), function(filter) {
                this.filters.add(filter);
            }, this);
        },

        /**
         * Reload the specified record.  The current store filters will also be applied.
         * @param {Rally.data.wsapi.Model/Number} record the record or id of the record to reload
         * @param {Object} options additional options to be applied to the {Ext.data.Operation}.
         * @param {Function} options.success callback - @deprecated - use returned promise instead
         * @param {Function} options.failure callback - @deprecated - use returned promise instead
         * @param {Object} options.scope callback scope - @deprecated - use returned promise instead
         * @return {Deft.Promise(Rally.data.wsapi.Model)}
         */
        reloadRecord: function(record, options) {
            options = options || {};
            var deferred = Ext.create('Deft.promise.Deferred');
            var operation = Ext.create('Ext.data.Operation', Ext.merge({
                action: 'read',
                filters: this.filters.getRange().concat({
                    property: 'ObjectID',
                    value: record.getId ? record.getId() : record
                }),
                limit: 1,
                enablePostGet: this.enablePostGet,
                requester: this,
                context: this.context
            }, options));

            operation.useShallowFetch = this.useShallowFetch;
            operation.fetch = this.fetch || this._getFetchFields(this.model);

            this.model.getProxy().read(operation, function(op) {
                if(op.wasSuccessful() && op.getRecords() && op.getRecords().length) {
                    var record = op.getRecords()[0];
                    Ext.callback(options.success, options.scope, [record]);
                    deferred.resolve(record);
                } else {
                    Ext.callback(options.failure, options.scope, [op]);
                    deferred.reject(op);
                }
            }, this);

            return deferred.promise;
        },

        hydrateModel: function() {
            return Rally.data.ModelFactory.getModel({
                type: this.initialConfig.model,
                context: this.context,
                wsapiVersion: this.wsapiVersion,
                requester: this.requester
            }).then({
                success: function(model) {
                    this.model = model;
                    if (this._customProxyInUse) {
                        this.proxy.setModel(model);
                    } else {
                        this.proxy = model.getProxy();
                    }

                    return model;
                },
                failure: function(msg) {
                    Ext.Error.raise(msg);
                    return msg;
                },
                scope: this
            });
        },

        sums: {},

        getSums: function() {
            return this.sums;
        },

        setSums: function(sums) {
            this.sums = sums;
        },

        _wrapCallbackWithPromise: function(options) {
            var me = this;

            var deferred = new Deft.Deferred(),
                promise = deferred.promise,
                originalCallback = options.callback;

            options.callback = function (records, operation, success) {

                if (operation.resultSet && operation.resultSet.sums) {
                    me.setSums(operation.resultSet.sums);
                }

                if(Ext.isFunction(originalCallback)){
                    originalCallback.apply(options.scope || me, arguments);
                }

                me._cleanUpCallbackForReload(options, originalCallback);
                me._cleanUpCallbackForReload(me.lastOptions, originalCallback);

                if (success) {
                    deferred.resolve(records, operation);
                } else {
                    deferred.reject(operation);
                }
                deferred = null;
                options = null;
                me = null;
            };

            return promise;
        },

        _cleanUpCallbackForReload: function(options, callback) {
            if (options) {
                options.callback = callback;
            }
        },

        _hydrateModelAndLoad: function(options){
            var deferred = new Deft.Deferred();

            this.hydrateModel().then({
                success: function(model) {
                    //Call load with original options
                    this.load(options).then({
                        success: Ext.bind(deferred.resolve, deferred),
                        failure: Ext.bind(deferred.reject, deferred)
                    });
                },
                scope: this
            });

            return deferred.promise;
        },

        _getModelFailureMessage: function(config) {
            return 'Invalid model type specified: ' + config.model;
        },

        /**
         * @private
         */
        filter: function() {
            //reset page to the first page
            this.currentPage = 1;
            this.callParent(arguments);
        },

        /**
         * @private
         * Override to add fetch param.
         */
        prefetch: function(options) {
            options = options || {};
            options.fetch = this.fetch.join(',');
            return this.callParent([options]);
        },

        /**
         * @private
         * override to turn off remote filtering when clearing filters
         */
        clearFilter: function(suppressEvent, suppressRemoteFilter) {
            var remoteFilter = this.remoteFilter;
            this.remoteFilter = suppressRemoteFilter ? false : remoteFilter;
            try {
                var results = this.callParent(arguments);
                return results;
            } finally {
                this.remoteFilter = remoteFilter;
            }
        },

        /**
         * @private
         * Returns all the items currently in the store. This method returns them in their "raw" form, as simple
         * objects in the same format as they were sent from the server. Use #getRecords to retrieve all the items
         * as proper instances of {@link Rally.data.Model}.
         *
         * NOTE: this method is deprecated, use #getRecords instead
         *
         * @return {Array} all of the items currently in the store
         */
        getItems: function() {
            return Ext.Array.pluck(this.getRange(), 'raw');
        },

        /**
         * Returns all the items currently in the store. Returns the items as instances of {@link Rally.data.Model}.
         * Use #getItems to retrieve the data in its raw form.
         *
         * @return {Rally.data.Model[]} The records currently in the store
         */
        getRecords: function() {
            var items = [];
            this.each(function(storeItem) {
                items.push(storeItem);
            });
            return items;
        },

        /**
         * Collect all records from the requested pages
         * @param {Object} options
         * @param {Number} options.startPage defaults to the first page
         * @param {Number} options.endPage defaults to the last page
         * @param {Function} options.callback
         * @param {Object} options.scope
         */
        loadPages: function(options) {
            var collectedRecords = [];
            var totalCount, pageCount;
            var pageNum = options.startPage || 1;

            var nextPage = Ext.bind(function() {
                if (options.endPage && pageNum > options.endPage) {
                    return Ext.callback(options.callback, options.scope, [collectedRecords]);
                }

                this.loadPage(pageNum, {
                    callback: function(records) {
                        //we have to wait until we load a page to ensure that the total count is available
                        totalCount = totalCount || this.getTotalCount();
                        pageCount = pageCount || Math.ceil(totalCount / this.pageSize) || 1;
                        options.endPage = options.endPage || pageCount;

                        collectedRecords = collectedRecords.concat(records);
                        pageNum++;
                        nextPage();
                    },
                    scope: this
                });
            }, this);

            nextPage();
        },

        /**
         * Create a new store using this store's initial configuration
         */
        clone: function() {
            var cloneConfig = Ext.merge(this.initialConfig, {sorters: this.getSorters()});
            return Ext.create(this.self, cloneConfig);
        },

        /**
         * @inheritdoc Rally.data.WsapiFindRecords.findExact
         */
        findExact: function(field, value, start) {
            return this.mixins.findRecords.findExact.call(this, field, value, start);
        },

        /**
         * Finds a record in the store that matches the given parameters.
         * @param {Rally.data.wsapi.Model/Object/String} field Record to find or field of the record used to look up by value
         *        If record or object is passed, the _ref field or property, respectively, will be used to search by
         * @param {Object} [value] The value of the field to find a matching record with
         */
        findExactRecord: function(field, value) {
            if (value) {
                return this.findRecord(field, value, undefined, undefined, undefined, true);
            } else {
                try {
                    var ref = Rally.util.Ref.getRefObject(field);
                    return this.findExactRecord('_ref', ref.getRelativeUri());
                } catch (e) {} // not a ref
            }
            return null;
        },

        /**
         * Finds a record in the store that matches the given parameters
         *
         * Unlike the find methods provided by Ext, this method ignores filters and it also performs only exact matches.
         *
         * @param {String} Name of the field to match against
         * @param {Object} [value] The value of the field to find a matching record with
         */
        findRecordEvenIfFiltered: function(field, value) {
            if (this.isFiltered() && this.snapshot && this.snapshot.items) {
                var match = _.find(this.snapshot.items, function(item) {
                    return item.get(field) === value;
                });
                return typeof match !== 'undefined' ? match : null;
            }
            return this.findExactRecord(field, value);
        },

        _setFetch: function(options) {
            this.fetch = options.fetch ||this.fetch || this._getFetchFields(this.model);
            if(this.enableRankFieldParameterAutoMapping) {
                this._convertFetch();
                this._convertSorters();
            }
        },

        _getFetchFields: function (model) {
            return _.pluck(_.isFunction(model.getNonCollectionFields) ? model.getNonCollectionFields() : [], 'name');
        },

        _convertFetch: function() {
            var fetch = this.fetch;

            if (Ext.isString(fetch)) {
                fetch = fetch.split(',');
            }

            if (Ext.isArray(fetch)) {
                this.fetch = _.uniq(
                    _.map(fetch, function (item) {
                        if (Rally.data.Ranker.isRankField(item)) {
                            item = Rally.data.Ranker.getRankField(this.model);
                        }
                        return item;
                    }, this)
                );
            }
        },

        _convertSorters: function() {
            this.sorters.each(function (sorter) {
                if (Rally.data.Ranker.isRankField(sorter.property)) {
                    sorter.property = Rally.data.Ranker.getRankField(this.model);
                }
            }, this);
        }
    });
})();