/**
 * The Store class encapsulates a client side cache of {@link Ext.data.Model Model} objects.
 * Stores load data via a {@link Ext.data.proxy.Proxy Proxy}, and also provide functions
 * for {@link #method-sort sorting}{@link #filter filtering} and querying the
 * {@link Ext.data.Model model} instances contained within it.
 *
 * Creating a Store is easy - we just tell it the Model and the Proxy to use for loading and saving
 * its data:
 *
 *      // Set up a model to use in our Store
 *      Ext.define('User', {
 *          extend: 'Ext.data.Model',
 *          fields: [
 *              {name: 'firstName', type: 'string'},
 *              {name: 'lastName',  type: 'string'},
 *              {name: 'age',       type: 'int'},
 *              {name: 'eyeColor',  type: 'string'}
 *          ]
 *      });
 *
 *      var myStore = Ext.create('Ext.data.Store', {
 *          model: 'User',
 *          proxy: {
 *              type: 'ajax',
 *              url: '/users.json',
 *              reader: {
 *                  type: 'json',
 *                  rootProperty: 'users'
 *              }
 *          },
 *          autoLoad: true
 *      });
 *
 * In the example above we configured an AJAX proxy to load data from the url '/users.json'.
 * We told our Proxy to use a {@link Ext.data.reader.Json JsonReader} to parse the response
 * from the server into Model object - {@link Ext.data.reader.Json see the docs on JsonReader}
 * for details.
 *
 * ## Inline data
 *
 * Stores can also load data inline. Internally, Store converts each of the objects we pass in
 * as {@link #cfg-data} into Model instances:
 *
 *      Ext.create('Ext.data.Store', {
 *          model: 'User',
 *          data : [
 *              {firstName: 'Peter',   lastName: 'Venkman'},
 *              {firstName: 'Egon',    lastName: 'Spengler'},
 *              {firstName: 'Ray',     lastName: 'Stantz'},
 *              {firstName: 'Winston', lastName: 'Zeddemore'}
 *          ]
 *      });
 *
 * Loading inline data using the method above is great if the data is in the correct format already
 * (e.g. it doesn't need to be processed by a {@link Ext.data.reader.Reader reader}). If your inline
 * data requires processing to decode the data structure, use a
 * {@link Ext.data.proxy.Memory MemoryProxy} instead (see the
 * {@link Ext.data.proxy.Memory MemoryProxy} docs for an example).
 *
 * Additional data can also be loaded locally using {@link #method-add}.
 * 
 * ## Dynamic Loading
 *
 * Stores can be dynamically updated by calling the {@link #method-load} method:
 *
 *     store.load({
 *         params: {
 *             group: 3,
 *             type: 'user'
 *         },
 *         callback: function(records, operation, success) {
 *             // do something after the load finishes
 *         },
 *         scope: this
 *     });
 *
 * Here a bunch of arbitrary parameters is passed along with the load request and a callback
 * function is set up to do something after the loading is over.
 *
 * ## Loading Nested Data
 *
 * Applications often need to load sets of associated data - for example a CRM system might load
 * a User and her Orders. Instead of issuing an AJAX request for the User and a series of additional
 * AJAX requests for each Order, we can load a nested dataset and allow the Reader to automatically
 * populate the associated models. Below is a brief example, see the {@link Ext.data.reader.Reader}
 * intro docs for a full explanation:
 *
 *      var store = Ext.create('Ext.data.Store', {
 *          autoLoad: true,
 *          model: "User",
 *          proxy: {
 *              type: 'ajax',
 *              url: 'users.json',
 *              reader: {
 *                  type: 'json',
 *                  rootProperty: 'users'
 *              }
 *          }
 *      });
 *
 * Which would consume a response like this:
 *
 *      {
 *          "users": [{
 *              "id": 1,
 *              "name": "Peter",
 *              "orders": [{
 *                  "id": 10,
 *                  "total": 10.76,
 *                  "status": "invoiced"
 *             },{
 *                  "id": 11,
 *                  "total": 13.45,
 *                  "status": "shipped"
 *             }]
 *          }]
 *      }
 *
 * See the {@link Ext.data.reader.Reader} intro docs for a full explanation.
 *
 * ## Filtering and Sorting
 *
 * Stores can be sorted and filtered - in both cases either remotely or locally. The
 * {@link #cfg-sorters} and {@link #cfg-filters} are held inside
 * {@link Ext.util.Collection Collection} instances to make them easy to manage. Usually it is
 * sufficient to either just specify sorters and filters in the Store configuration or call
 * {@link #method-sort} or {@link #filter}:
 *
 *      var store = Ext.create('Ext.data.Store', {
 *          model: 'User',
 *          sorters: [{
 *              property: 'age',
 *              direction: 'DESC'
 *          }, {
 *              property: 'firstName',
 *              direction: 'ASC'
 *          }],
 *
 *          filters: [{
 *              property: 'firstName',
 *              value: /Peter/
 *          }]
 *      });
 *
 * The new Store will keep the configured sorters and filters in the Collection instances mentioned
 * above. By default, sorting and filtering are both performed locally by the Store - see
 * {@link #remoteSort} and {@link #remoteFilter} to allow the server to perform these operations
 * instead.
 *
 * Filtering and sorting after the Store has been instantiated is also easy. Calling {@link #filter}
 * adds another filter to the Store and automatically filters the dataset (calling {@link #filter}
 * with no arguments simply re-applies all existing filters).
 *
 *     store.filter('eyeColor', 'Brown');
 *
 * Change the sorting at any time by calling {@link #method-sort}:
 *
 *     store.sort('height', 'ASC');
 *
 * Note that all existing sorters will be removed in favor of the new sorter data (if
 * {@link #method-sort} is called with no arguments, the existing sorters are just reapplied
 * instead of being removed). To keep existing sorters and add new ones, just add them to the
 * Collection:
 *
 *     store.sorters.add(new Ext.util.Sorter({
 *         property : 'shoeSize',
 *         direction: 'ASC'
 *     }));
 *
 *     store.sort();
 *
 * ## Registering with StoreManager
 *
 * Any Store that is instantiated with a {@link #storeId} will automatically be registered with
 * the {@link Ext.data.StoreManager StoreManager}. This makes it easy to reuse the same store
 * in multiple views:
 *
 *     //this store can be used several times
 *     Ext.create('Ext.data.Store', {
 *         model: 'User',
 *         storeId: 'usersStore'
 *     });
 *
 *     new Ext.List({
 *         store: 'usersStore',
 *         //other config goes here
 *     });
 *
 *     new Ext.view.View({
 *         store: 'usersStore',
 *         //other config goes here
 *     });
 *
 * ## Further Reading
 *
 * Stores are backed up by an ecosystem of classes that enables their operation. To gain a full
 * understanding of these pieces and how they fit together, see:
 *
 *   - {@link Ext.data.proxy.Proxy Proxy} - overview of what Proxies are and how they are used
 *   - {@link Ext.data.Model Model} - the core class in the data package
 *   - {@link Ext.data.reader.Reader Reader} - used by any subclass of
 *     {@link Ext.data.proxy.Server ServerProxy} to read a response
 */
Ext.define('Ext.data.Store', {
    extend: 'Ext.data.ProxyStore',
 
    alias: 'store.store',
 
    mixins: [
        'Ext.data.LocalStore'
    ],
 
    // Required classes must be loaded before the definition callback runs
    // The class definition callback creates a dummy Store which requires that
    // all the classes below have been loaded.
    requires: [
        'Ext.data.Model',
        'Ext.data.proxy.Ajax',
        'Ext.data.reader.Json',
        'Ext.data.writer.Json',
 
        // This ensures that we have Ext.util.Collection and all of its requirements.
        'Ext.util.GroupCollection',
        'Ext.util.DelayedTask'
    ],
 
    uses: [
        'Ext.data.StoreManager',
        'Ext.util.Grouper'
    ],
 
    config: {
        /**
         * @cfg {Object[]/Ext.data.Model[]} data
         * Array of Model instances or data objects to load locally. See "Inline data"
         * above for details.
         */
        data: undefined, // undefined so the applier is always called
 
        /**
         * @cfg {Boolean} [clearRemovedOnLoad=true]
         * `true` to clear anything in the {@link #removed} record collection when the store loads.
         */
        clearRemovedOnLoad: true,
 
        /**
        * @cfg {Boolean} [clearOnPageLoad=true]
        * True to empty the store when loading another page via {@link #loadPage},
        * {@link #nextPage} or {@link #previousPage}. Setting to false keeps existing records,
        * allowing large data sets to be loaded one page at a time but rendered all together.
        */
        clearOnPageLoad: true,
 
        /**
         * @cfg {Ext.data.Model} [associatedEntity]
         * The owner of this store if the store is used as part of an association.
         *
         * @private
         */
        associatedEntity: null,
 
        /**
         * @cfg {Ext.data.schema.Role} [role]
         * The role for the {@link #associatedEntity}.
         *
         * @private
         */
        role: null,
 
        /**
         * @cfg {Ext.data.Session} session
         * The session for this store. By specifying a session, it ensures any records that are
         * added to this store are also included in the session. This store does not become a member
         * of the session itself.
         *
         * @since  5.0.0
         */
        session: null
    },
 
    /**
     * @property {Ext.util.Collection} data
     * The `data` property is a `Collection` which holds this store's local cache of records.
     * @private
     * @readonly
     */
 
    /**
     * @private
     * Used as a parameter to loadRecords
     */
    addRecordsOptions: {
        addRecords: true
    },
 
    /**
     * @property {Number} loadCount
     * The number of times records have been loaded into the store. This includes loads via
     * {@link #loadData} & {@link #loadRecords}.
     * @readonly
     */
    loadCount: 0,
 
    /**
     * `true` once the store has loaded data from the server.
     * @property {Boolean} complete
     *
     * @private
     */
    complete: false,
 
    moveMapCount: 0,
 
    /**
     * Creates the store.
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        var me = this,
            data;
 
        if (config) {
            if (config.buffered) {
                //<debug>
                if (this.self !== Ext.data.Store) {
                    Ext.raise('buffered config not supported on derived Store classes. ' +
                                    'Please derive from Ext.data.BufferedStore.');
                }
                //</debug>
 
                // Hide this from Cmd
                /* eslint-disable-next-line dot-notation */
                return new Ext.data['BufferedStore'](config);
            }
 
            //<debug>
            if (config.remoteGroup) {
                Ext.log.warn('Ext.data.Store: remoteGroup has been removed. ' +
                             'Use remoteSort instead.');
            }
            //</debug>
        }
 
        /**
         * @event beforeprefetch
         * Fires before a prefetch occurs. Return `false` to cancel.
         * @param {Ext.data.Store} this 
         * @param {Ext.data.operation.Operation} operation The associated operation.
         */
        /**
         * @event groupchange
         * Fired whenever the grouping in the grid changes.
         * @param {Ext.data.Store} store The store.
         * @param {Ext.util.Grouper} grouper The grouper object.
         */
        /**
         * @event groupschange
         * Fired whenever the multi grouping in the grid changes.
         * @param {Ext.data.Store} store The store.
         * @param {Ext.util.GrouperCollection} groupers The groupers collection.
         */
        /**
         * @event prefetch
         * Fires whenever records have been prefetched.
         * @param {Ext.data.Store} this 
         * @param {Ext.data.Model[]} records An array of records.
         * @param {Boolean} successful `true` if the operation was successful.
         * @param {Ext.data.operation.Operation} operation The associated operation.
         */
        /**
         * @event filterchange
         * Fired whenever the filter set changes.
         * @param {Ext.data.Store} store The store.
         * @param {Ext.util.Filter[]} filters The array of Filter objects.
         */
 
        me.callParent([config]);
 
        // See applyData for the details.
        data = me.inlineData;
 
        if (data) {
            delete me.inlineData;
            me.loadInlineData(data);
        }
 
    },
 
    /**
     * @method getData
     * Returns the store's records.
     *
     * **Note:** If your store has been filtered, getData() will return a filtered
     * collection.  Use `getData().{@link Ext.util.Collection#getSource getSource()}` to
     * fetch all unfiltered records.
     *
     * @return {Ext.util.Collection} An Ext.util.Collection of records
     * (an empty Collection if no records are held by the store).
     */
 
    /**
     * @method setData
     * Loads an array of data directly into the Store.
     *
     * setData() is ideal if your data's format is already in its appropriate format (e.g. it
     * doesn't need to be processed by a reader). If your data's structure requires processing,
     * use a {@link Ext.data.proxy.Memory MemoryProxy} or {@link #loadRawData}.
     *
     * Use {@link #loadData}{@link #method-add}, or {@link #insert} if records need to be
     * appended to the current recordset.
     *
     * @param {Ext.data.Model[]/Object[]} data Array of data to load. Any non-model instances
     * will be cast into model instances first.
     */
 
    applyData: function(data, dataCollection) {
        // We bring up the Collection for records which forms the bottom of the config
        // dependency graph. The appliers for "filters" and "sorters" depend on "data"
        // and "remoteFilter" and "remoteSort" depend on both in their updaters.
        var me = this;
 
        // Ensure that we process our Model config first.
        me.getFields();
        me.getModel();
 
        // We might be configured with a Collection instance
        if (data && data.isCollection) {
            data.setRootProperty('data');
            dataCollection = data;
            dataCollection.addObserver(this);
 
            // Perform a load postprocess if the incoming collection is loaded.
            if (data.getCount()) {
                me.afterLoadRecords(data.items);
 
                // This is not fired by afterLoadRecords because loadRecords
                // which calls afterLoadRecords is a public API which simply adds some
                // records. This situation here though, is anaologous to a load.
                if (me.hasListeners.load) {
                    me.fireEvent('load', me, data.items, true);
                }
            }
        }
        else {
            if (!dataCollection) {
                dataCollection = me.constructDataCollection();
            }
 
            if (data) {
                if (me.isInitializing) {
                    // When data is configured on the instance of a Store we must wait for
                    // all the things to initialize (sorters, filters, groupers) so that we
                    // can properly process that data. All of those appliers, however, depend
                    // on the dataCollection (us) to get booted up first so we must defer
                    // this back to after initConfig. In previous versions this was hacked
                    // at by the constructor via "config.data" but "data" can also be set on
                    // the Ext.define level so best to pick it up here and store aside to be
                    // finished in the constructor.
                    me.inlineData = data;
                }
                else {
                    // If we are not constructing the Store than a setData call needs to be
                    // equivalent to the legacy loadData method with respect to events that fire,
                    // etc.
                    me.loadData(data);
                }
            }
        }
 
        return dataCollection;
    },
 
    loadInlineData: function(data) {
        var me = this,
            proxy = me.getProxy();
 
        if (proxy && proxy.isMemoryProxy) {
            proxy.setData(data);
 
            // Allow a memory proxy to trigger a load initially
            me.suspendEvents();
            me.read();
            me.resumeEvents();
 
        }
        else {
            // We make it silent because we don't want to fire a refresh event
            me.removeAll(true);
 
            // We don't want to fire addrecords event since we will be firing
            // a refresh event later which will already take care of updating
            // any views bound to this store
            me.suspendEvents();
            me.loadData(data);
            me.resumeEvents();
        }
    },
 
    /**
     * @method insert
     * @inheritdoc Ext.data.LocalStore#insert
     */
 
    onCollectionAdd: function(collection, info) {
        this.loadCount = this.loadCount || 1;
        this.onCollectionAddItems(collection, info.items, info);
    },
 
    onCollectionFilterAdd: function(collection, items) {
        this.onCollectionAddItems(collection, items);
    },
 
    onCollectionAddItems: function(collection, records, info) {
        var me = this,
            len = records.length,
            lastChunk = info ? !info.next : false,
 
            // Must use class-specific removed property.
            // Regular Stores add to the "removed" property on remove.
            // TreeStores are having records removed all the time; node collapse removes.
            // TreeStores add to the "removedNodes" property onNodeRemove
            removed = me.removed,
            ignoreAdd = me.ignoreCollectionAdd,
            session = me.getSession(),
            replaced = info && info.replaced,
            i, sync, record, replacedItems;
 
        // Collection add changes the items reference of the collection, and that array
        // object if directly referenced by Ranges. The ranges have to refresh themselves
        // upon add.
        if (me.activeRanges) {
            me.syncActiveRanges();
        }
 
        for (= 0; i < len; ++i) {
            record = records[i];
 
            if (session) {
                session.adopt(record);
            }
 
            // If ignoring, we don't want to do anything other than pull
            // the added records into the session    
            if (!ignoreAdd) {
                record.join(me);
 
                if (removed && removed.length) {
                    Ext.Array.remove(removed, record);
                }
 
                sync = sync || record.phantom || record.dirty;
            }
        }
 
        if (ignoreAdd) {
            return;
        }
 
        if (replaced) {
            replacedItems = [];
 
            do {
                Ext.Array.push(replacedItems, replaced.items);
                replaced = replaced.next;
            } while (replaced);
 
            me.setMoving(replacedItems, true);
        }
 
        if (info) {
            // If this is a replacement operation, there will have been a
            // previous call to onCollectionRemove which will have fired no
            // events in anticipation of a final refresh event.
            // Here is where we inform interested parties of all the changes.
            if (info.replaced) {
                if (lastChunk) {
                    me.fireEvent('datachanged', me);
                    me.fireEvent('refresh', me);
                }
            }
            else {
                me.fireEvent('add', me, records, info.at);
 
                // If there is a next property, that means there is another range that needs
                // to be removed after this. Wait until everything is gone before firing
                // datachanged since it should be a bulk operation
                if (lastChunk) {
                    me.fireEvent('datachanged', me);
                }
            }
        }
 
        if (replacedItems) {
            me.setMoving(replacedItems, false);
        }
 
        // Addition means a sync is needed.
        me.needsSync = me.needsSync || sync;
    },
 
    onCollectionBeforeItemChange: function(collection, info) {
        var record = info.item,
            modifiedFieldNames = info.modified || null,
            type = info.meta;
 
        // This is currently intended to be private
        this.fireEvent('beforeupdate', this, record, type, modifiedFieldNames, info);
    },
 
    // If our source collection informs us that a filtered out item has changed, we must still
    // fire the events...
    onCollectionFilteredItemChange: function() {
        this.onCollectionItemChange.apply(this, arguments);
    },
 
    onCollectionItemChange: function(collection, info) {
        var me = this,
            record = info.item,
            modifiedFieldNames = info.modified || null,
            type = info.meta;
 
        if (me.fireChangeEvent(record)) {
            // Inform any interested parties that a record has been mutated.
            // This will be invoked on TreeStores in which the invoking record
            // is an descendant of a collapsed node, and so *will not be contained by this store
            me.onUpdate(record, type, modifiedFieldNames, info);
            me.fireEvent('update', me, record, type, modifiedFieldNames, info);
            me.fireEvent('datachanged', me);
        }
    },
 
    afterChange: function(record, modifiedFieldNames, type) {
        this.getData().itemChanged(record, modifiedFieldNames || null, undefined, type);
    },
 
    afterCommit: function(record, modifiedFieldNames) {
        this.afterChange(record, modifiedFieldNames, Ext.data.Model.COMMIT);
    },
 
    afterEdit: function(record, modifiedFieldNames) {
        this.needsSync = this.needsSync || record.dirty;
        this.afterChange(record, modifiedFieldNames, Ext.data.Model.EDIT);
    },
 
    afterReject: function(record) {
        this.afterChange(record, null, Ext.data.Model.REJECT);
    },
 
    afterDrop: function(record) {
        this.getData().remove(record);
    },
 
    afterErase: function(record) {
        this.removeFromRemoved(record);
    },
 
    /**
     * @method add
     * @inheritdoc Ext.data.LocalStore#add
     */
 
    /**
     * (Local sort only) Inserts the passed Record into the Store at the index where it
     * should go based on the current sort information.
     *
     * @param {Ext.data.Record} record 
     */
    addSorted: function(record) {
        var me = this,
            remote = me.getRemoteSort(),
            data = me.getData(),
            index;
 
        if (remote) {
            data.setSorters(me.getSorters());
        }
 
        index = data.findInsertionIndex(record);
 
        if (remote) {
            data.setSorters(null);
        }
 
        return me.insert(index, record);
    },
 
    /**
     * Removes the specified record(s) from the Store, firing the {@link #event-remove}
     * event for the removed records.
     *
     * After all records have been removed a single `datachanged` is fired.
     *
     * @param {Ext.data.Model/Ext.data.Model[]/Number/Number[]} records Model instance or
     * array of instances to remove or an array of indices from which to remove records.
     * @param isMove (private)
     * @param silent (private)
     */
    remove: function(records, isMove, silent) {
        var me = this,
            data = me.getDataSource(),
            len, i, toRemove, record;
 
        if (records) {
            if (records.isModel) {
                if (data.indexOf(records) > -1) {
                    toRemove = [records];
                    len = 1;
                }
                else {
                    len = 0;
                }
            }
            else {
                toRemove = [];
 
                for (= 0, len = records.length; i < len; ++i) {
                    record = records[i];
 
                    if (record && record.isEntity) {
                        if (!data.contains(record)) {
                            continue;
                        }
                    }
                    else if (!(record = data.getAt(record))) { // an index
                        continue;
                    }
 
                    toRemove.push(record);
                }
 
                len = toRemove.length;
            }
        }
 
        if (!len) {
            return [];
        }
 
        me.removeIsMove = isMove === true;
        me.removeIsSilent = silent;
        data.remove(toRemove);
        me.removeIsSilent = false;
 
        return toRemove;
    },
 
    onCollectionRemove: function(collection, info) {
        var me = this,
            // Must use class-specific removed property.
            // Regular Stores add to the "removed" property on remove.
            // TreeStores are having records removed all the time; node collapse removes.
            // TreeStores add to the "removedNodes" property onNodeRemove
            removed = me.removed,
            records = info.items,
            len = records.length,
            index = info.at,
            replacement = info.replacement,
            /* eslint-disable-next-line max-len */
            isMove = me.removeIsMove || (replacement && Ext.Array.equals(records, replacement.items)),
            silent = me.removeIsSilent,
            lastChunk = !info.next,
            data = me.getDataSource(),
            i, record;
 
        if (me.ignoreCollectionRemove) {
            return;
        }
 
        if (replacement) {
            me.setMoving(replacement.items, true);
        }
 
        for (= len - 1; i >= 0; i--) {
            record = records[i];
 
            // If the data contains the record, that means the record is filtered out, so
            // it's not being removed, nor should it be unjoined
            if (!data.contains(record)) {
                // Don't push interally moving, or phantom (client side only), 
                // erasing (informing server through its own proxy) records
                if (removed && !isMove && !record.phantom && !record.erasing) {
                    // Store the index the record was removed from so that rejectChanges can
                    // re-insert at the correct place.
                    // The record's index property won't do, as that is the index in the overall
                    // dataset when Store is buffered.
                    record.removedFrom = index + i;
                    removed.push(record);
 
                    // Removal of a non-phantom record which is NOT erasing (informing the server
                    // through its own proxy) requires that the store be synced at some point.
                    me.needsSync = true;
                }
                else {
                    // Only unjoin if we're not being pushed into the removed collection. We still
                    // have an interest in that record otherwise.
                    record.unjoin(me);
                }
            }
        }
 
        if (!silent) {
            // If this removal is just the first part of a replacement operation,
            // do not fire the events now.
            //
            // onCollectionAddItems will fire a refresh event, and convert multiple
            // remove and add operations to an atomic refresh event.
            // This will provide a better UI update.
            // Also, focus can only be preserved around one operation, so
            // editing a field which is the sorted field could result in 
            // incorrect focus..
            if (!replacement || !replacement.items.length) {
                me.fireEvent('remove', me, records, index, isMove);
 
                // If there is a next property, that means there is another range that needs
                // to be removed after this. Wait until everything is gone before firing datachanged
                // since it should be a bulk operation
                if (lastChunk) {
                    me.fireEvent('datachanged', me);
                }
            }
        }
 
        if (replacement) {
            me.setMoving(replacement.items, false);
        }
    },
 
    onFilterEndUpdate: function() {
        var me = this;
 
        if (me.destroying || me.destroyed) {
            return;
        }
 
        // Filtering changes the items reference of the collection, and that array
        // object if directly referenced by Ranges. The ranges have to refresh themselves
        // upon add.
        if (me.activeRanges) {
            me.syncActiveRanges();
        }
 
        me.callParent(arguments);
        me.callObservers('Filter');
    },
 
    /**
     * Removes the model instance(s) at the given index
     * @param {Number} index The record index
     * @param {Number} [count=1] The number of records to delete
     */
    removeAt: function(index, count) {
        var data = this.getData();
 
        // Sanity check input.
        index = Math.max(index, 0);
 
        if (index < data.length) {
            if (arguments.length === 1) {
                count = 1;
            }
            else if (!count) {
                return;
            }
 
            data.removeAt(index, count);
        }
    },
 
    /**
     * Removes all unfiltered items from the store.  Filtered records will not be removed.
     * Individual record `{@link #event-remove}` events are not fired by this method.
     *
     * @param {Boolean} [silent=false] Pass `true` to prevent the `{@link #event-clear}` event
     * from being fired.
     * @return {Ext.data.Model[]} The removed records.
     */
    removeAll: function(silent) {
        var me = this,
            data = me.getData(),
            records = data.getRange();
 
        // We want to remove and mute any events here
        if (data.length) {
            // Explicit true here, we never want to fire remove events
            me.removeIsSilent = true;
            me.callObservers('BeforeRemoveAll');
            data.removeAll();
            me.removeIsSilent = false;
 
            if (!silent) {
                me.fireEvent('clear', me, records);
                me.fireEvent('datachanged', me);
            }
 
            me.callObservers('AfterRemoveAll', [!!silent]);
        }
 
        return records;
    },
 
    /**
     * Make a set of records be current in the store. This means that unneeded records
     * will be removed and new records will be added.
     * @param {Ext.data.Model[]} records The records to be current in the store.
     *
     * @private
     */
    setRecords: function(records) {
        var count = this.getCount();
 
        ++this.loadCount;
 
        if (count) {
            this.getData().splice(0, count, records);
        }
        else {
            this.add(records);
        }
    },
 
    /**
     * This method is basically the same as the JavaScript Array splice method.
     *
     * Negative indexes are interpreted starting at the end of the collection. That is,
     * a value of -1 indicates the last item, or equivalent to `length - 1`.
     *
     * @param {Number} index The index at which to add or remove items.
     * @param {Number/Object[]} toRemove The number of items to remove or an array of the
     * items to remove.
     * @param  {Object[]} [toAdd] The items to insert at the given `index`.
     * @private
     */
    splice: function(index, toRemove, toAdd) {
        return this.getData().splice(index, toRemove, toAdd);
    },
 
    /**
     * @protected
     * Called internally when a Proxy has completed a load request
     */
    onProxyLoad: function(operation) {
        var me = this,
            resultSet = operation.getResultSet(),
            records = operation.getRecords(),
            successful = operation.wasSuccessful();
 
        if (me.destroyed) {
            return;
        }
 
        if (resultSet) {
            me.totalCount = resultSet.getTotal();
        }
 
        if (successful) {
            records = me.processAssociation(records);
            me.loadRecords(records, operation.getAddRecords() ? { addRecords: true } : undefined);
            me.attachSummaryRecord(resultSet);
        }
        else {
            me.loading = false;
        }
 
        if (me.hasListeners.load) {
            me.fireEvent('load', me, records, successful, operation);
        }
 
        me.callObservers('AfterLoad', [records, successful, operation]);
    },
 
    onProxyWrite: function(operation) {
        if (operation.wasSuccessful()) {
            this.attachSummaryRecord(operation.getResultSet());
        }
 
        this.callParent([operation]);
    },
 
    // private
    filterDataSource: function(fn) {
        var source = this.getDataSource(),
            items = source.items,
            len = items.length,
            ret = [],
            i;
 
        for (= 0; i < len; i++) {
            if (fn.call(source, items[i])) {
                ret.push(items[i]);
            }
        }
 
        return ret;
    },
 
    getNewRecords: function() {
        return this.filterDataSource(this.filterNew);
    },
 
    getRejectRecords: function() {
        return this.filterDataSource(this.filterRejects);
    },
 
    getUpdatedRecords: function() {
        return this.filterDataSource(this.filterUpdated);
    },
 
    /**
     * Loads an array of data straight into the Store.
     *
     * Using this method is great if the data is in the correct format already (e.g. it doesn't
     * need to be processed by a reader). If your data requires processing to decode the data
     * structure, use a {@link Ext.data.proxy.Memory MemoryProxy} or {@link #loadRawData}.
     *
     * @param {Ext.data.Model[]/Object[]} data Array of data to load. Any non-model instances will
     * be cast into model instances first.
     * @param {Boolean} [append=false] `true` to add the records to the existing records in the
     * store, `false` to remove the old ones first.
     */
    loadData: function(data, append) {
        var me = this,
            length = data.length,
            newData = [],
            i;
 
        // make sure each data element is an Ext.data.Model instance
        for (= 0; i < length; i++) {
            newData.push(me.createModel(data[i]));
        }
 
        newData = me.processAssociation(newData);
 
        me.loadRecords(newData, append ? me.addRecordsOptions : undefined);
    },
 
    /**
     * Loads data via the bound Proxy's reader
     *
     * Use this method if you are attempting to load data and want to utilize the configured data
     * reader.
     *
     * As of 4.2, this method will no longer fire the {@link #event-load} event.
     *
     * @param {Object[]} data The full JSON object you'd like to load into the Data store.
     * @param {Boolean} [append=false] `true` to add the records to the existing records in the
     * store, `false` to remove the old ones first.
     * 
     * @return {Boolean} `true` if the reader processed the records correctly. See
     * {@link Ext.data.reader.Reader#successProperty}. If the reader did not process the records,
     * nothing will be added.
     */
    loadRawData: function(data, append) {
        var me = this,
            session = me.getSession(),
            result, records, success;
 
        /* eslint-disable-next-line max-len */
        result = me.getProxy().getReader().read(data, session ? { recordCreator: session.recordCreator } : undefined);
        records = result.getRecords();
        success = result.getSuccess();
 
        if (success) {
            me.totalCount = result.getTotal();
            me.loadRecords(records, append ? me.addRecordsOptions : undefined);
        }
 
        return success;
    },
 
    /**
     * Loads an array of {@link Ext.data.Model model} instances into the store, fires the
     * datachanged event. This should only usually be called internally when loading from the
     * {@link Ext.data.proxy.Proxy Proxy}, when adding records manually use {@link #method-add}
     * instead
     * @param {Ext.data.Model[]} records The array of records to load
     * @param {Object} options 
     * @param {Boolean} [options.addRecords=false] Pass `true` to add these records to the existing
     * records, `false` to remove the Store's existing records first.
     */
    loadRecords: function(records, options) {
        var me = this,
            data = me.getData(),
            addRecords, skipSort;
 
        if (options) {
            addRecords = options.addRecords;
        }
 
        if (!me.getRemoteSort() && !me.getSortOnLoad()) {
            skipSort = true;
            data.setAutoSort(false);
        }
 
        if (!addRecords) {
            me.clearData(true);
        }
 
        // Clear the flag AFTER the stores collection has been cleared down so that
        // observers of that collection know that it was due to a load, and a refresh is imminent.
        me.loading = false;
 
        me.ignoreCollectionAdd = true;
        me.callObservers('BeforePopulate');
        data.add(records);
        me.ignoreCollectionAdd = false;
 
        if (skipSort) {
            data.setAutoSort(true);
        }
 
        me.afterLoadRecords(records);
    },
 
    afterLoadRecords: function(records) {
        var me = this,
            length = records.length,
            i;
 
        for (= 0; i < length; i++) {
            records[i].join(me);
        }
 
        if (!me.isEmptyStore) {
            ++me.loadCount;
            me.complete = true;
        }
 
        if (me.hasListeners.datachanged) {
            me.fireEvent('datachanged', me);
        }
 
        if (me.hasListeners.refresh) {
            me.fireEvent('refresh', me);
        }
 
        me.callObservers('AfterPopulate');
    },
 
    // PAGING METHODS
    /**
     * Loads a given 'page' of data by setting the start and limit values appropriately. Internally
     * this just causes a normal load operation, passing in calculated 'start' and 'limit' params.
     * @param {Number} page The number of the page to load.
     * @param {Object} [options] See options for {@link #method-load}.
     */
    loadPage: function(page, options) {
        var me = this,
            size = me.getPageSize();
 
        me.currentPage = page;
 
        // Copy options into a new object so as not to mutate passed in objects
        options = Ext.apply({
            page: page,
            start: (page - 1) * size,
            limit: size,
            addRecords: !me.getClearOnPageLoad()
        }, options);
 
        me.read(options);
    },
 
    /**
     * Loads the next 'page' in the current data set
     * @param {Object} options See options for {@link #method-load}
     */
    nextPage: function(options) {
        this.loadPage(this.currentPage + 1, options);
    },
 
    /**
     * Loads the previous 'page' in the current data set
     * @param {Object} options See options for {@link #method-load}
     */
    previousPage: function(options) {
        this.loadPage(this.currentPage - 1, options);
    },
 
    /**
     * @private
     */
    clearData: function(isLoad) {
        var me = this,
            removed = me.removed,
            data = me.getDataSource(),
            clearRemovedOnLoad = me.getClearRemovedOnLoad(),
            needsUnjoinCheck = removed && isLoad && !clearRemovedOnLoad,
            records, record, i, len;
 
        // We only have to do the unjoining if not buffered. PageMap will unjoin its records when
        // it clears itself.
        // There is a potential for a race condition in stores configured with autoDestroy: true;
        // if loading was initiated but didn't complete by the time the store is destroyed,
        // the data MC may not have been created yet so we have to check for its existence
        // here and below.
        if (data) {
            records = data.items;
 
            for (= 0, len = records.length; i < len; ++i) {
                record = records[i];
 
                if (needsUnjoinCheck && Ext.Array.contains(removed, record)) {
                    continue;
                }
 
                record.unjoin(me);
            }
 
            me.ignoreCollectionRemove = true;
            me.callObservers('BeforeClear');
            data.removeAll();
            me.ignoreCollectionRemove = false;
            me.callObservers('AfterClear');
        }
 
        if (removed && (!isLoad || clearRemovedOnLoad)) {
            removed.length = 0;
        }
    },
 
    onIdChanged: function(rec, oldId, newId) {
        this.getData().updateKey(rec, oldId);
 
        // This event is used internally
        this.fireEvent('idchanged', this, rec, oldId, newId);
    },
 
    /**
     * Commits all Records with {@link #getModifiedRecords outstanding changes}. To handle updates
     * for changes, subscribe to the Store's {@link #event-update update event}, and perform
     * updating when the third parameter is Ext.data.Record.COMMIT.
     */
    commitChanges: function() {
        var me = this,
            recs = me.getModifiedRecords(),
            len = recs.length,
            i = 0;
 
        Ext.suspendLayouts();
        me.beginUpdate();
 
        for (; i < len; i++) {
            recs[i].commit();
        }
 
        me.cleanRemoved();
 
        me.endUpdate();
        Ext.resumeLayouts(true);
 
        /**
         * @private
         * @event commit
         * Fired when all changes were committed and the Store is clean.
         *
         * **Note** Used internally.
         *
         * @param {Ext.data.Store} store The Store object
         */
        me.fireEvent('commit', me);
    },
 
    filterNewOnly: function(item) {
        return item.phantom === true;
    },
 
    filterRejects: function(item) {
        return item.phantom || item.dirty;
    },
 
    /**
     * {@link Ext.data.Model#reject Rejects} outstanding changes on all {@link #getModifiedRecords
     * modified records} and re-insert any records that were removed locally. Any phantom records
     * will be removed.
     */
    rejectChanges: function() {
        var me = this,
            recs = me.getRejectRecords(),
            len = recs.length,
            i, rec, toRemove, sorted, data, currentAutoSort;
 
        Ext.suspendLayouts();
        me.beginUpdate();
 
        for (= 0; i < len; i++) {
            rec = recs[i];
 
            if (rec.phantom) {
                toRemove = toRemove || [];
                toRemove.push(rec);
            }
            else {
                rec.reject();
            }
        }
 
        if (toRemove) {
            me.remove(toRemove);
 
            for (= 0, len = toRemove.length; i < len; ++i) {
                toRemove[i].reject();
            }
        }
 
        // Restore removed records back to their original positions.
        recs = me.getRawRemovedRecords();
 
        if (recs) {
            len = recs.length;
            sorted = !me.getRemoteSort() && me.isSorted();
 
            if (sorted) {
                // Temporarily turn off sorting so .reject() doesn't attempt to sort the record.
                // It would throw b/c the record isn't yet in its collection.
                data = me.getData();
                currentAutoSort = data.getAutoSort();
 
                data.setAutoSort(false);
            }
 
            for (= len - 1; i >= 0; i--) {
                rec = recs[i];
                rec.reject();
 
                if (!sorted) {
                    me.insert(rec.removedFrom || 0, rec);
                }
            }
 
            if (sorted) {
                // Turn sorting back on so the collection is auto-sorted when added.
                data.setAutoSort(currentAutoSort);
                me.add(recs);
            }
 
            // Don't need to call cleanRemoved because we've re-added everything, don't
            // need to unjoin the store
            recs.length = 0;
        }
 
        me.endUpdate();
        Ext.resumeLayouts(true);
 
        /**
         * @private
         * @event reject
         * Fired when all changes were rejected and the Store is clean.
         *
         * **Note** Used internally.
         *
         * @param {Ext.data.Store} store The Store object
         */
        me.fireEvent('reject', me);
    },
 
    doDestroy: function() {
        var me = this,
            task = me.loadTask,
            data = me.getData(),
            source = data.getSource();
 
        // clearData ensures everything is unjoined
        me.clearData();
        me.setSession(null);
        me.observers = null;
 
        if (task) {
            task.cancel();
            me.loadTask = null;
        }
 
        if (source) {
            source.destroy();
        }
 
        me.callParent();
    },
 
    /**
     * Change summary functions on multiple fields on the store model
     *
     * @param {Object} values Object where keys are field names and values are summary types
     */
    setFieldsSummaries: function(values) {
        var me = this,
            model = me.model.getSummaryModel(),
            key;
 
        if (!model || me.isDestroyed) {
            return;
        }
 
        for (key in values) {
            model.setSummaryField(key, values[key]);
        }
 
        if (me.getRemoteSummary()) {
            me.reload();
        }
        else {
            me.getSummaryRecord().calculateSummary(me.getData().items);
            me.recalculateSummaries(me.getGroups());
            me.fireEvent('summarieschanged', me);
            me.fireEvent('datachanged', me);
        }
    },
 
    /**
     * Change summary function on the specified field of the store model
     *
     * @param {String} field 
     * @param {String/Ext.data.summary.Base} summary
     */
    setFieldSummary: function(field, summary) {
        var me = this,
            model = me.model.getSummaryModel();
 
        if (!model || me.isDestroyed) {
            return;
        }
 
        model.setSummaryField(field, summary);
 
        if (me.getRemoteSummary()) {
            me.reload();
        }
        else {
            me.getSummaryRecord().calculateSummary(me.getData().items);
            me.recalculateSummaries(me.getGroups());
            me.fireEvent('summarieschanged', me);
            me.fireEvent('datachanged', me);
        }
    },
 
    recalculateSummaries: function(groups) {
        var groupCount = groups ? groups.length : 0,
            i, group;
 
        for (= 0; i < groupCount; i++) {
            group = groups.items[i];
 
            group.recalculateSummaries();
 
            this.recalculateSummaries(group.getGroups());
        }
    },
 
    privates: {
        commitOptions: {
            commit: true
        },
 
        attachSummaryRecord: function(resultSet) {
            if (!resultSet) {
                return;
            }
 
            /* eslint-disable-next-line vars-on-top */
            var me = this,
                summary = resultSet.getSummaryData(),
                groupers = me.getGroupers(),
                current = me.summaryRecord,
                commitOptions = me.commitOptions,
                changed = false,
                groups, len, i, rec, group, children, child;
 
            if (summary) {
                changed = true;
 
                if (current) {
                    // we need to clear the data object otherwise fields
                    // that are populated on the client side but are not
                    // calculated on the server may not be removed.
                    current.data = {};
                    current.set(summary.data, commitOptions);
                }
                else {
                    me.summaryRecord = summary;
                    summary.isRemote = true;
                }
            }
 
            if (groupers && groupers.length) {
                changed = true;
                summary = resultSet.getGroupData();
 
                if (summary) {
                    groups = me.getGroups();
 
                    for (= 0, len = summary.length; i < len; ++i) {
                        rec = summary[i];
                        // The summaries are in fact records. For multiple groupers the records
                        // should only fill in the dataIndex of all groupers above it.
                        group = groups.getItemGroup(rec);
 
                        if (group) {
                            children = group.getGroups();
 
                            while (children) {
                                child = children.getItemGroup(rec);
 
                                if (child) {
                                    // if the remote summary record is undefined on the grouper
                                    // property then this child group is not the right one
                                    if (Ext.isDefined(rec.data[child.getGrouper().getProperty()])) {
                                        group = child;
                                        children = group.getGroups();
                                    }
                                    else {
                                        children = null;
                                    }
                                }
                                else {
                                    children = null;
                                }
                            }
 
                            delete(rec.data.id);
                            current = group.getGroupRecord();
                            current.set(rec.data, commitOptions);
                            current.isRemote = true;
 
                            current = group.getSummaryRecord();
                            current.set(rec.data, commitOptions);
                            current.isRemote = true;
                        }
                    }
                }
            }
 
            if (changed) {
                if (me.hasListeners.remotesummarieschanged) {
                    me.fireEvent('remotesummarieschanged', me);
                }
            }
        },
 
        /**
         * Similar to a load, however no records are added to the store. This is useful
         * in allowing the developer to decide what to do with the new records.
         * @param {Object} [options] See {@link #method-load load options}.
         *
         * @private
         */
        fetch: function(options) {
            var me = this,
                operation;
 
            options = Ext.apply({}, options);
 
            this.setLoadOptions(options);
 
            operation = Ext.apply({
                internalScope: me,
                internalCallback: me.onFetch,
                scope: me
            }, options);
            operation = this.createOperation('read', operation);
            operation.execute();
        },
 
        onFetch: function(operation) {
            var me = this,
                records = operation.getRecords(),
                successful = operation.wasSuccessful();
 
            if (me.destroyed) {
                return;
            }
 
            if (me.hasListeners.load) {
                me.fireEvent('load', me, records, successful, operation);
            }
        },
 
        fireChangeEvent: function(record) {
            return this.getDataSource().contains(record);
        },
 
        onBeforeLoad: function(operation) {
            this.callObservers('BeforeLoad', [operation]);
        },
 
        onRemoteFilterSet: function(filters, remoteFilter) {
            if (filters) {
                this.getData().setFilters(remoteFilter ? null : filters);
            }
 
            this.callParent([filters, remoteFilter]);
        },
 
        onRemoteSortSet: function(sorters, remoteSort) {
            var data = this.getData();
 
            if (sorters) {
                data.setSorters(remoteSort ? null : sorters);
            }
 
            data.setAutoGroup(!remoteSort);
 
            this.callParent([sorters, remoteSort]);
        },
 
        /**
         * Checks whether records are being moved within the store. This can be used in conjunction
         * with the {@link #event-add} and {@link #event-remove} events to determine whether
         * the records are being removed/added or just having the position changed.
         * @param {Ext.data.Model[]/Ext.data.Model} [records] The record(s).
         * @param {Object} [getMap] (private)
         * @return {Number} The number of records being moved. `0` if no records are moving.
         * If records are passed the number will refer to how many of the passed records are moving.
         *
         * @private
         */
        isMoving: function(records, getMap) {
            var map = this.moveMap,
                moving = 0,
                len, i;
 
            if (map) {
                if (records) {
                    if (Ext.isArray(records)) {
                        for (= 0, len = records.length; i < len; ++i) {
                            moving += map[records[i].id] ? 1 : 0;
                        }
                    }
                    else if (map[records.id]) {
                        ++moving;
                    }
                }
                else {
                    moving = getMap ? map : this.moveMapCount;
                }
            }
 
            return moving;
        },
 
        setLoadOptions: function(options) {
            // Only add grouping options if grouping is remote
            var me = this,
                pageSize = me.getPageSize(),
                summaries = [],
                groupers = me.getGroupers(false),
                session, grouper, model, fields, field, len, i;
 
            if (me.getRemoteSort() || me.getRemoteSummary()) {
                if (!options.grouper) {
                    grouper = me.getGrouper();
 
                    if (grouper) {
                        options.grouper = grouper;
                    }
                }
                else if (!options.groupers) {
                    if (groupers && groupers.length) {
                        options.groupers = groupers.getRange();
                    }
                }
            }
 
            if (me.getRemoteSummary()) {
                // extract summary fields from the model
                model = me.getModel().getSummaryModel();
                fields = model.getFields();
                len = fields.length;
 
                for (= 0; i < len; i++) {
                    // fields that are sent as groupers should not be sent as summaries
                    // otherwise when the response comes back we can't link groups to results
                    field = fields[i];
 
                    if (groupers && !groupers.get(field.name)) {
                        summaries.push(field);
                    }
                }
 
                options.summaries = summaries;
            }
 
            if (pageSize || 'start' in options || 'limit' in options || 'page' in options) {
                options.page = options.page != null ? options.page : me.currentPage;
 
                options.start = (options.start !== undefined)
                    ? options.start
                    : (options.page - 1) * pageSize;
 
                options.limit = options.limit != null ? options.limit : pageSize;
 
                me.currentPage = options.page;
            }
 
            options.addRecords = options.addRecords || false;
 
            if (!options.recordCreator) {
                session = me.getSession();
 
                if (session) {
                    options.recordCreator = session.recordCreator;
                }
            }
 
            me.callParent([options]);
        },
 
        setMoving: function(records, isMoving) {
            var me = this,
                map = me.moveMap || (me.moveMap = {}),
                len = records.length,
                i, id;
 
            for (= 0; i < len; ++i) {
                id = records[i].id;
 
                if (isMoving) {
                    if (map[id]) {
                        ++map[id];
                    }
                    else {
                        map[id] = 1;
                        ++me.moveMapCount;
                    }
                }
                else {
                    if (--map[id] === 0) {
                        delete map[id];
                        --me.moveMapCount;
                    }
                }
            }
 
            if (me.moveMapCount === 0) {
                me.moveMap = null;
            }
        },
 
        processAssociation: function(records) {
            var me = this,
                associatedEntity = me.getAssociatedEntity();
 
            if (associatedEntity) {
                records = me.getRole().processLoad(me, associatedEntity, records, me.getSession());
            }
 
            return records;
        }
    }
 
    // Provides docs from the mixin
    /**
     * @method each
     * @inheritdoc Ext.data.LocalStore#each
     */
 
    /**
     * @method collect
     * @inheritdoc Ext.data.LocalStore#collect
     */
 
    /**
     * @method getById
     * @inheritdoc Ext.data.LocalStore#getById
     */
 
    /**
     * @method getByInternalId
     * @inheritdoc Ext.data.LocalStore#getByInternalId
     */
 
    /**
     * @method indexOf
     * @inheritdoc Ext.data.LocalStore#indexOf
     */
 
    /**
     * @method indexOfId
     * @inheritdoc Ext.data.LocalStore#indexOfId
     */
 
    /**
     * @method queryBy
     * @inheritdoc Ext.data.LocalStore#queryBy
     */
 
    /**
     * @method query
     * @inheritdoc Ext.data.LocalStore#query
     */
 
    /**
     * @method first
     * @inheritdoc Ext.data.LocalStore#first
     */
 
    /**
     * @method last
     * @inheritdoc Ext.data.LocalStore#last
     */
 
    /**
     * @method sum
     * @inheritdoc Ext.data.LocalStore#sum
     */
 
    /**
     * @method count
     * @inheritdoc Ext.data.LocalStore#count
     */
 
    /**
     * @method min
     * @inheritdoc Ext.data.LocalStore#min
     */
 
    /**
     * @method max
     * @inheritdoc Ext.data.LocalStore#max
     */
 
    /**
     * @method average
     * @inheritdoc Ext.data.LocalStore#average
     */
 
    /**
     * @method aggregate
     * @inheritdoc Ext.data.LocalStore#aggregate
     */
});