/**
 * This class manages uniquely keyed objects such as {@link Ext.data.Model records} or
 * {@link Ext.Component components}.
 *
 * ## Keys
 *
 * Unlike `Ext.util.MixedCollection` this class can only manage objects whose key can be
 * extracted from the instance. That is, this class does not support "external" keys. This
 * makes this class more efficient because it does not need to track keys in parallel with
 * items. It also means key-to-item lookup will be optimal and never need to perform a
 * linear search.
 *
 * ### Extra Keys
 *
 * In some cases items may need to be looked up by multiple property values. To enable this
 * there is the `extraKeys` config.
 *
 * For example, to quickly look up items by their "name" property:
 *
 *      var collection = new Ext.util.Collection({
 *          extraKeys: {
 *              byName: 'name' // based on "name" property of each item
 *          }
 *      });
 *
 * ## Ranges
 *
 * When methods accept index arguments to indicate a range of items, these are either an
 * index and a number of items or a "begin" and "end" index.
 *
 * In the case of "begin" and "end", the "end" is the first item outside the range. This
 * definition makes it simple to expression empty ranges because "length = end - begin".
 *
 * ### Negative Indices
 *
 * When an item index is provided, negative values are treated as offsets from the end of
 * the collection. In other words the follow are equivalent:
 *
 *      +---+---+---+---+---+---+
 *      |   |   |   |   |   |   |
 *      +---+---+---+---+---+---+
 *        0   1   2   3   4   5
 *       -6  -5  -4  -3  -2  -1
 *
 * ## Legacy Classes
 *
 * The legacy classes `Ext.util.MixedCollection' and `Ext.util.AbstractMixedCollection`
 * may be needed if external keys are required, but for all other situations this class
 * should be used instead.
 */
Ext.define('Ext.util.Collection', {
    mixins: [
        'Ext.mixin.Observable'
    ],
 
    requires: [
        'Ext.util.CollectionKey',
        'Ext.util.Filter',
        'Ext.util.Sorter',
        'Ext.util.Grouper'
    ],
    
    uses: [
        'Ext.util.SorterCollection',
        'Ext.util.FilterCollection',
        'Ext.util.GroupCollection'
    ],
 
    /**
     * @property {Boolean} isCollection
     * `true` in this class to identify an object as an instantiated Collection, or subclass
     * thereof.
     * @readonly
     */
    isCollection: true,
 
    config: {
        autoFilter: true,
 
        /**
         * @cfg {Boolean} [autoSort=true] `true` to maintain sorted order when items
         * are added regardless of requested insertion point, or when an item mutation
         * results in a new sort position.
         *
         * This does not affect a filtered Collection's reaction to mutations of the source
         * Collection. If sorters are present when the source Collection is mutated, this Collection's
         * sort order will always be maintained.
         * @private
         */
        autoSort: true,
 
        /**
         * @cfg {Boolean} [autoGroup=true] `true` to sort by the grouper
         * @private
         */
        autoGroup: true,
 
        /**
         * @cfg {Function} decoder
         * A function that can convert newly added items to a proper type before being
         * added to this collection.
         */
        decoder: null,
 
        /**
         * @cfg {Object} extraKeys
         * One or more `Ext.util.CollectionKey` configuration objects or key properties.
         * Each property of the given object is the name of the `CollectionKey` instance
         * that is stored on this collection. The value of each property configures the
         * `CollectionKey` instance.
         *
         *      var collection = new Ext.util.Collection({
         *          extraKeys: {
         *              byName: 'name' // based on "name" property of each item
         *          }
         *      });
         *
         * Or equivalently:
         *
         *      var collection = new Ext.util.Collection({
         *          extraKeys: {
         *              byName: {
         *                  property: 'name'
         *              }
         *          }
         *      });
         *
         * To provide a custom key extraction function instead:
         *
         *      var collection = new Ext.util.Collection({
         *          extraKeys: {
         *              byName: {
         *                  keyFn: function (item) {
         *                      return item.name;
         *                  }
         *              }
         *          }
         *      });
         *
         * Or to call a key getter method from each item:
         *
         *      var collection = new Ext.util.Collection({
         *          extraKeys: {
         *              byName: {
         *                  keyFn: 'getName'
         *              }
         *          }
         *      });
         *
         * To use the above:
         *
         *      var item = collection.byName.get('somename');
         *
         * **NOTE** Either a `property` or `keyFn` must be be specified to define each
         * key.
         * @since 5.0.0
         */
        extraKeys: null,
 
        /**
         * @cfg {Array/Ext.util.FilterCollection} filters
         * The collection of {@link Ext.util.Filter Filters} for this collection. At the
         * time a collection is created `filters` can be specified as a unit. After that
         * time the normal `setFilters` method can also be given a set of replacement
         * filters for the collection.
         *
         * Individual filters can be specified as an `Ext.util.Filter` instance, a config
         * object for `Ext.util.Filter` or simply a function that will be wrapped in a
         * instance with its {@link Ext.util.Filter#filterFn filterFn} set.
         *
         * For fine grain control of the filters collection, call `getFilters` to return
         * the `Ext.util.Collection` instance that holds this collection's filters.
         *
         *      var collection = new Ext.util.Collection();
         *      var filters = collection.getFilters(); // an Ext.util.FilterCollection
         *
         *      function legalAge (item) {
         *          return item.age >= 21;
         *      }
         *
         *      filters.add(legalAge);
         *
         *      //...
         *
         *      filters.remove(legalAge);
         *
         * Any changes to the `filters` collection will cause this collection to adjust
         * its items accordingly (if `autoFilter` is `true`).
         * @since 5.0.0
         */
        filters: null,
 
        /**
         * @cfg {Object} grouper
         * A configuration object for this collection's {@link Ext.util.Grouper grouper}.
         *
         * For example, to group items by the first letter of the last name:
         *
         *      var collection = new Ext.util.Collection({
         *          grouper: {
         *              groupFn: function (item) {
         *                  return item.lastName.substring(0, 1);
         *              }
         *          }
         *      });
         */
        grouper: null,
        
        /**
         * @cfg {Ext.util.GroupCollection} groups
         * The collection of to hold each group container. This collection is created and
         * removed dynamically based on `grouper`. Application code should only need to
         * call `getGroups` to retrieve the collection and not `setGroups`.
         */
        groups: null,
 
        /**
         * @cfg {Object} groupConfig
         * A default configuration to be passed to any groups created by the
         * {@link Ext.util.GroupCollection}. See {@link #groups}.
         *
         * @private
         * @since 6.5.0
         */
        groupConfig: null,
 
        /**
         * @cfg {String} rootProperty
         * The root property to use for aggregation, filtering and sorting. By default
         * this is `null` but when containing things like {@link Ext.data.Model records}
         * this config would likely be set to "data" so that property names are applied
         * to the fields of each record.
         */
        rootProperty: null,
 
        /**
         * @cfg {Array/Ext.util.SorterCollection} sorters
         * Array of {@link Ext.util.Sorter sorters} for this collection. At the time a
         * collection is created the `sorters` can be specified as a unit. After that time
         * the normal `setSorters` method can be also be given a set of replacement
         * sorters.
         *
         * Individual sorters can be specified as an `Ext.util.Sorter` instance, a config
         * object for `Ext.util.Sorter` or simply the name of a property by which to sort.
         *
         * An alternative way to extend the sorters is to call the `sort` method and pass
         * a property or sorter config to add to the sorters.
         *
         * For fine grain control of the sorters collection, call `getSorters` to return
         * the `Ext.util.Collection` instance that holds this collection's sorters.
         *
         *      var collection = new Ext.util.Collection();
         *      var sorters = collection.getSorters(); // an Ext.util.SorterCollection
         *
         *      sorters.add('name');
         *
         *      //...
         *
         *      sorters.remove('name');
         *
         * Any changes to the `sorters` collection will cause this collection to adjust
         * its items accordingly (if `autoSort` is `true`).
         *
         * @since 5.0.0
         */
        sorters: null,
        
        /**
         * @cfg {Number} [multiSortLimit=3]
         * The maximum number of sorters which may be applied to this Sortable when using
         * the "multi" insertion position when adding sorters.
         *
         * New sorters added using the "multi" insertion position are inserted at the top
         * of the sorters list becoming the new primary sort key.
         *
         * If the sorters collection has grown to longer then **`multiSortLimit`**, then
         * the it is trimmed.
         */
        multiSortLimit: 3,
        
        /**
         * @cfg {String} defaultSortDirection
         * The default sort direction to use if one is not specified.
         */
        defaultSortDirection: 'ASC',
 
        /**
         * @cfg {Ext.util.Collection} source
         * The base `Collection`. This collection contains the items to which filters
         * are applied to populate this collection. In this configuration, only the
         * root `source` collection can have items truly added or removed.
         * @since 5.0.0
         */
        source: null,
 
        /**
         * @cfg {Boolean} trackGroups
         * `true` to track individual groups in a Ext.util.GroupCollection
         * @private
         */
        trackGroups: true
    },
    
    /**
     * @property {Number} generation
     * Mutation counter which is incremented when the collection changes.
     * @readonly
     * @since 5.0.0
     */
    generation: 0,
 
    /**
     * @property {Object} indices
     * An object used as map to get the index of an item.
     * @private
     * @since 5.0.0
     */
    indices: null,
 
    /**
     * @property {Number} indexRebuilds
     * The number of times the `indices` have been rebuilt. This is for diagnostic use.
     * @private
     * @readonly
     * @since 5.0.0
     */
    indexRebuilds: 0,
 
    /**
     * @property {Number} updating
     * A counter that is increased by `beginUpdate` and decreased by `endUpdate`. When
     * this transitions from 0 to 1 the `{@link #event-beginupdate beginupdate}` event is
     * fired. When it transitions back from 1 to 0 the `{@link #event-endupdate endupdate}`
     * event is fired.
     * @readonly
     * @since 5.0.0
     */
    updating: 0,
    
    /**
     * @property {Boolean} grouped
     * A read-only flag indicating if this object is grouped.
     * @readonly
     */
    grouped: false,
 
    /**
     * @property {Boolean} sorted
     * A read-only flag indicating if this object is sorted. This flag may not be correct
     * during an update of the sorter collection but will be correct before `onSortChange`
     * is called. This flag is `true` if `grouped` is `true` because the collection is at
     * least sorted by the `grouper`.
     * @readonly
     */
    sorted: false,
    
    /**
     * @property {Boolean} filtered
     * A read-only flag indicating if this object is filtered.
     * @readonly
     */
    filtered: false,
 
    /**
     * @private
     * Priority that is used for endupdate listeners on the filters and sorters.
     * set to a very high priority so that our processing of these events takes place prior
     * to user code - data must already be filtered/sorted when the user's handler runs
     */
    $endUpdatePriority: 1001,
 
    /**
     * @private
     * `true` to destroy the sorter collection on destroy.
     */
    manageSorters: true,
 
    /**
     * @event add
     * Fires after items have been added to the collection.
     *
     * All `{@link #event-add add}` and `{@link #event-remove remove}` events occur between
     * `{@link #event-beginupdate beginupdate}` and `{@link #event-endupdate endupdate}`
     * events so it is best to do only the minimal amount of work in response to these
     * events and move the more expensive side-effects to an `endupdate` listener.
     *
     * @param {Ext.util.Collection} collection The collection being modified.
     *
     * @param {Object} details An object describing the addition.
     *
     * @param {Number} details.at The index in the collection where the add occurred.
     *
     * @param {Object} details.atItem The item after which the new items were inserted or
     * `null` if at the beginning of the collection.
     * 
     * @param {Object[]} details.items The items that are now added to the collection.
     *
     * @param {Array} [details.keys] If available this array holds the keys (extracted by
     * `getKey`) for each item in the `items` array.
     *
     * @param {Object} [details.next] If more `{@link #event-add add}` events are in queue
     * to be delivered this is a reference to the `details` instance for the next
     * `{@link #event-add add}` event. This will only be the case when the collection is
     * sorted as the new items often need to be inserted at multiple locations to maintain
     * the sort. In this case, all of the new items have already been added not just those
     * described by the first `{@link #event-add add}` event.
     *
     * @param {Object} [details.replaced] If this addition has a corresponding set of
     * `{@link #event-remove remove}` events this reference holds the `details` object for
     * the first `remove` event. That `details` object may have a `next` property if there
     * are multiple associated `remove` events.
     *
     * @since 5.0.0
     */
 
    /**
     * @event beginupdate
     * Fired before changes are made to the collection. This event fires when the
     * `beginUpdate` method is called and the counter it manages transitions from 0 to 1.
     *
     * All `{@link #event-add add}` and `{@link #event-remove remove}` events occur between
     * `{@link #event-beginupdate beginupdate}` and `{@link #event-endupdate endupdate}`
     * events so it is best to do only the minimal amount of work in response to these
     * events and move the more expensive side-effects to an `endupdate` listener.
     *
     * @param {Ext.util.Collection} collection The collection being modified.
     *
     * @since 5.0.0
     */
 
    /**
     * @event endupdate
     * Fired after changes are made to the collection. This event fires when the `endUpdate`
     * method is called and the counter it manages transitions from 1 to 0.
     *
     * All `{@link #event-add add}` and `{@link #event-remove remove}` events occur between
     * `{@link #event-beginupdate beginupdate}` and `{@link #event-endupdate endupdate}`
     * events so it is best to do only the minimal amount of work in response to these
     * events and move the more expensive side-effects to an `endupdate` listener.
     *
     * @param {Ext.util.Collection} collection The collection being modified.
     *
     * @since 5.0.0
     */
 
    /**
     * @event beforeitemchange
     * This event fires before an item change is reflected in the collection. This event
     * is always followed by an `itemchange` event and, depending on the change, possibly
     * an `add`, `remove` and/or `updatekey` event.
     *
     * @param {Ext.util.Collection} collection The collection being modified.
     *
     * @param {Object} details An object describing the change.
     *
     * @param {Object} details.item The item that has changed.
     *
     * @param {String} details.key The key of the item that has changed.
     *
     * @param {Boolean} details.filterChanged This is `true` if the filter status of the
     * `item` has changed. That is, the item was previously filtered out and is no longer
     * or the opposite.
     *
     * @param {Ext.util.Group} details.group The group containing the `item`. **(since 6.5.1)**
     *
     * @param {Boolean} details.groupChanged This is `true` if the item is moving between
     * groups. See also the `group` and `oldGroup` properties. **(since 6.5.1)**
     *
     * @param {Boolean} details.keyChanged This is `true` if the item has changed keys. If
     * so, check `oldKey` for the old key. An `updatekey` event will follow.
     *
     * @param {Boolean} details.indexChanged This is `true` if the item needs to move to
     * a new index in the collection due to sorting. The index can be seen in `index`.
     * The old index is in `oldIndex`.
     *
     * @param {String[]} [details.modified] If known this property holds the array of names
     * of the modified properties of the item.
     *
     * @param {Boolean} [details.filtered] This value is `true` if the item will be filtered
     * out of the collection.
     *
     * @param {Number} [details.index] The new index in the collection for the item if
     * the item is being moved (see `indexChanged`). If the item is being removed due to
     * filtering, this will be -1.
     *
     * @param {Ext.util.Group} details.oldGroup The group that previously contained the
     * `item`. **(since 6.5.1)**
     *
     * @param {Number} [details.oldIndex] The old index in the collection for the item if
     * the item is being moved (see `indexChanged`). If the item was being removed due to
     * filtering, this will be -1.
     *
     * @param {Object} [details.oldKey] The old key for the `item` if the item's key has
     * changed (see `keyChanged`).
     *
     * @param {Boolean} [details.wasFiltered] This value is `true` if the item was filtered
     * out of the collection.
     *
     * @since 5.0.0
     */
 
    /**
     * @event itemchange
     * This event fires after an item change is reflected in the collection. This event
     * always follows a `beforeitemchange` event and its corresponding `add`, `remove`
     * and/or `updatekey` events.
     *
     * @param {Ext.util.Collection} collection The collection being modified.
     *
     * @param {Object} details An object describing the change.
     *
     * @param {Object} details.item The item that has changed.
     *
     * @param {String} details.key The key of the item that has changed.
     *
     * @param {Boolean} details.filterChanged This is `true` if the filter status of the
     * `item` has changed. That is, the item was previously filtered out and is no longer
     * or the opposite.
     *
     * @param {Ext.util.Group} details.group The group containing the `item`. **(since 6.5.1)**
     *
     * @param {Boolean} details.groupChanged This is `true` if the item is moving between
     * groups. See also the `group` and `oldGroup` properties. **(since 6.5.1)**
     *
     * @param {Object} details.keyChanged This is `true` if the item has changed keys. If
     * so, check `oldKey` for the old key. An `updatekey` event will have been sent.
     *
     * @param {Boolean} details.indexChanged This is `true` if the item was moved to a
     * new index in the collection due to sorting. The index can be seen in `index`.
     * The old index is in `oldIndex`.
     *
     * @param {String[]} [details.modified] If known this property holds the array of names
     * of the modified properties of the item.
     *
     * @param {Boolean} [details.filtered] This value is `true` if the item is filtered
     * out of the collection.
     *
     * @param {Number} [details.index] The new index in the collection for the item if
     * the item has been moved (see `indexChanged`). If the item is removed due to
     * filtering, this will be -1.
     *
     * @param {Ext.util.Group} details.oldGroup The group that previously contained the
     * `item`. **(since 6.5.1)**
     *
     * @param {Number} [details.oldIndex] The old index in the collection for the item if
     * the item has been moved (see `indexChanged`). If the item was being removed due to
     * filtering, this will be -1.
     *
     * @param {Object} [details.oldKey] The old key for the `item` if the item's key has
     * changed (see `keyChanged`).
     *
     * @param {Boolean} [details.wasFiltered] This value is `true` if the item was filtered
     * out of the collection.
     *
     * @since 5.0.0
     */
 
    /**
     * @event refresh
     * This event fires when the collection has changed entirely. This event is fired in
     * cases where the collection's filter is updated or the items are sorted. While the
     * items previously in the collection may remain the same, the order at a minimum has
     * changed in ways that cannot be simply translated to other events.
     *
     * @param {Ext.util.Collection} collection The collection being modified.
     */
 
    /**
     * @event remove
     * Fires after items have been removed from the collection. Some properties of this
     * object may not be present if calculating them is deemed too expensive. These are
     * marked as "optional".
     *
     * All `{@link #event-add add}` and `{@link #event-remove remove}` events occur between
     * `{@link #event-beginupdate beginupdate}` and `{@link #event-endupdate endupdate}`
     * events so it is best to do only the minimal amount of work in response to these
     * events and move the more expensive side-effects to an `endupdate` listener.
     *
     * @param {Ext.util.Collection} collection The collection being modified.
     *
     * @param {Object} details An object describing the removal.
     *
     * @param {Number} details.at The index in the collection where the removal occurred.
     *
     * @param {Object[]} details.items The items that are now removed from the collection.
     *
     * @param {Array} [details.keys] If available this array holds the keys (extracted by
     * `getKey`) for each item in the `items` array.
     *
     * @param {Object} [details.map] If available this is a map keyed by the key of each
     * item in the `items` array. This will often contain all of the items being removed
     * and not just the items in the range described by this event. The value held in this
     * map is the item.
     *
     * @param {Object} [details.next] If more `{@link #event-remove remove}` events are in
     * queue to be delivered this is a reference to the `details` instance for the next
     * remove event.
     *
     * @param {Object} [details.replacement] If this removal has a corresponding
     * `{@link #event-add add}` taking place this reference holds the `details` object for
     * that `add` event. If the collection is sorted, the new items are pre-sorted but the
     * `at` property for the `replacement` will **not** be correct. The new items will be
     * added in one or more chunks at their proper index.
     *
     * @since 5.0.0
     */
 
    /**
     * @event sort
     * This event fires after the contents of the collection have been sorted.
     *
     * @param {Ext.util.Collection} collection The collection being sorted.
     */
    
    /**
     * @event beforesort
     * @private
     * This event fires before the contents of the collection have been sorted.
     *
     * @param {Ext.util.Collection} collection The collection being sorted.
     * @param {Ext.util.Sorter[]} sorters Array of sorters applied to the Collection.
     */
 
    /**
     * @event updatekey
     * Fires after the key for an item has changed.
     *
     * @param {Ext.util.Collection} collection The collection being modified.
     *
     * @param {Object} details An object describing the update.
     *
     * @param {Object} details.item The item whose key has changed.
     *
     * @param {Object} details.newKey The new key for the `item`.
     *
     * @param {Object} details.oldKey The old key for the `item`.
     *
     * @since 5.0.0
     */
 
    constructor: function (config) {
        var me = this;
        
        //<debug>
        me.callParent([config]);
        //</debug>
 
        /**
         * @property {Object[]} items
         * An array containing the items.
         * @private
         * @since 5.0.0
         */
        me.items = [];
 
        /**
         * @property {Object} map
         * An object used as a map to find items based on their key.
         * @private
         * @since 5.0.0
         */
        me.map = {};
 
        /**
         * @property {Number} length
         * The count of items in the collection.
         * @readonly
         * @since 5.0.0
         */
        me.length = 0;
 
        /**
         * @cfg {Function} [keyFn]
         * A function to retrieve the key of an item in the collection. If provided,
         * this replaces the default `getKey` method. The default `getKey` method handles
         * items that have either an "id" or "_id" property or failing that a `getId`
         * method to call.
         * @since 5.0.0
         */
        if (config && config.keyFn) {
            me.getKey = config.keyFn;
        }
 
        me.mixins.observable.constructor.call(me, config);
    },
 
    /**
     * Destroys this collection. This is only necessary if this collection uses a `source`
     * collection as that relationship will keep a reference from the `source` to this
     * collection and potentially leak memory.
     * @since 5.0.0
     */
    destroy: function () {
        var me = this,
            filters = me._filters,
            sorters = me._sorters,
            groups = me._groups;
 
        if (filters) {
            filters.destroy();
            me._filters = null;
        }
 
        if (sorters) {
            // Set to false here so updateSorters doesn't trigger
            // the template methods
            me.grouped = me.sorted = false;
            me.setSorters(null);
            if (me.manageSorters) {
                sorters.destroy();
            }
        }
 
        if (groups) {
            groups.destroy();
            me._groups = null;
        }
        me.setSource(null);
        me.observers = me.items = me.map = null;
 
        me.callParent();
    },
 
    /**
     * Adds an item to the collection. If the item already exists or an item with the
     * same key exists, the old item will be removed and the new item will be added to
     * the end.
     *
     * This method also accepts an array of items or simply multiple items as individual
     * arguments. The following 3 code sequences have the same end result:
     *
     *      // Call add() once per item (not optimal - best avoided):
     *      collection.add(itemA);
     *      collection.add(itemB);
     *      collection.add(itemC);
     *      collection.add(itemD);
     *
     *      // Call add() with each item as an argument:
     *      collection.add(itemA, itemB, itemC, itemD);
     *
     *      // Call add() with the items as an array:
     *      collection.add([ itemA, itemB, itemC, itemD ]);
     *
     * The first form should be avoided where possible because the collection and all
     * parties "watching" it will be updated 4 times.
     *
     * @param {Object/Object[]} item The item or items to add.
     * @return {Object/Object[]} The item or items added.
     * @since 5.0.0
     */
    add: function (item) {
        var me = this,
            items = me.decodeItems(arguments, 0),
            ret = items;
 
        if (items.length) {
            me.splice(me.length, 0, items);
            ret = (items.length === 1) ? items[0] : items;
        }
 
        return ret;
    },
    
    /**
     * Adds an item to the collection while removing any existing items. Similar to {@link #method-add}.
     * @param {Object/Object[]} item The item or items to add.
     * @return {Object/Object[]} The item or items added.
     * @since 5.0.0
     */
    replaceAll: function() {
        var me = this,
            ret, items;
        
        items = me.decodeItems(arguments, 0);
        ret = items;
 
        if (items.length) {
            me.splice(0, me.length, items);
            ret = (items.length === 1) ? items[0] : items;
        } else {
            me.removeAll();
        }
 
        return ret;
    },
 
    /**
     * Returns the result of the specified aggregation operation against all items in this
     * collection.
     *
     * This method is not typically called directly because there are convenience methods
     * for each of the supported `operation` values. These are:
     *
     *   * **average** - Returns the average value.
     *   * **bounds**  - Returns an array of `[min, max]`.
     *   * **max**     - Returns the maximum value or `undefined` if empty.
     *   * **min**     - Returns the minimum value or `undefined` if empty.
     *   * **sum**     - Returns the sum of all values.
     *
     * For example:
     *
     *      result = collection.aggregate('age', 'sum');
     *
     *      result = collection.aggregate('age', 'sum', 2, 10); // the 8 items at index 2
     *
     * To provide a custom operation function:
     *
     *      function averageAgeOfMinors (items, values) {
     *          var sum = 0,
     *              count = 0;
     *
     *          for (var i = 0; i < values.length; ++i) {
     *              if (values[i] < 18) {
     *                  sum += values[i];
     *                  ++count;
     *              }
     *          }
     *
     *          return count ? sum / count : 0;
     *      }
     *
     *      result = collection.aggregate('age', averageAgeOfMinors);
     *
     * @param {String} property The name of the property to aggregate from each item.
     * @param {String/Function} operation The operation to perform.
     * @param {Array} operation.items The items on which the `operation` function is to
     * operate.
     * @param {Array} operation.values The values on which the `operation` function is to
     * operate.
     * @param {Number} [begin] The index of the first item in `items` to include in the
     * aggregation.
     * @param {Number} [end] The index at which to stop aggregating `items`. The item at
     * this index will *not* be included in the aggregation.
     * @param {Object} [scope] The `this` pointer to use if `operation` is a function.
     * Defaults to this collection.
     * @return {Object} 
     */
    aggregate: function (property, operation, begin, end, scope) {
        var me = this,
            args = Ext.Array.slice(arguments);
 
        args.unshift(me.items);
 
        return me.aggregateItems.apply(me, args);
    },
    
    /**
     * See {@link #aggregate}. The functionality is the same, however the aggregates are
     * provided per group. Assumes this collection has an active {@link #grouper}.
     * 
     * @param {String} property The name of the property to aggregate from each item.
     * @param {String/Function} operation The operation to perform.
     * @param {Array} operation.items The items on which the `operation` function is to
     * operate.
     * @param {Array} operation.values The values on which the `operation` function is to
     * operate.
     * @param {Object} [scope] The `this` pointer to use if `operation` is a function.
     * Defaults to this collection.
     * @return {Object} 
     */
    aggregateByGroup: function(property, operation, scope) {
        var groups = this.getGroups();
        return this.aggregateGroups(groups, property, operation, scope);
    },
    
    /**
     * Returns the result of the specified aggregation operation against the given items.
     * For details see `aggregate`.
     *
     * @param {Array} items The items to aggregate.
     * @param {String} property The name of the property to aggregate from each item.
     * @param {String/Function} operation The operation to perform.
     * @param {Array} operation.items The items on which the `operation` function is to
     * operate.
     * @param {Array} operation.values The values on which the `operation` function is to
     * operate.
     * @param {Number} [begin] The index of the first item in `items` to include in the
     * aggregation.
     * @param {Number} [end] The index at which to stop aggregating `items`. The item at
     * this index will *not* be included in the aggregation.
     * @param {Object} [scope] The `this` pointer to use if `operation` is a function.
     * Defaults to this collection.
     * 
     * @private
     * @return {Object} 
     */
    aggregateItems: function (items, property, operation, begin, end, scope) {
        var me = this,
            range = Ext.Number.clipIndices(items.length, [ begin, end ]),
                
            // Only extract items into new array if a subset is required
            subsetRequested = (begin !== 0 && end !== items.length),
 
            i, j,
            rangeLen,
            root, value,
            values, valueItems;
 
        begin = range[0];
        end = range[1];
 
        if (!Ext.isFunction(operation)) {
            operation = me._aggregators[operation];
            return operation.call(me, items, begin, end, property, me.getRootProperty());
        }
 
        root = me.getRootProperty();
 
        // Preallocate values array with known set size.
        // valueItems can be just the items array is a subset has not been requested
        values = new Array(rangeLen);
        valueItems = subsetRequested ? new Array(rangeLen) : items;
 
        // Collect the extracted property values and the items for passing to the operation.
        for (= begin, j = 0; i < end; ++i, j++) {
            if (subsetRequested) {
                valueItems[j] = value = items[i];
            }
            values[j] = (root ? value[root] : value)[property];
        }
 
        return operation.call(scope || me, items, values, 0);
    },
    
    /**
     * Aggregates a set of groups.
     * @param {Ext.util.GroupCollection} groups The groups
     * @param {String} property The name of the property to aggregate from each item.
     * @param {String/Function} operation The operation to perform.
     * @param {Array} operation.values The values on which the `operation` function is to
     * operate.
     * @param {Array} operation.items The items on which the `operation` function is to
     * operate.
     * @param {Number} operation.index The index in `items` at which the `operation`
     * function is to start. The `values.length` indicates the number of items involved.
     * @param {Object} [scope] The `this` pointer to use if `operation` is a function.
     * Defaults to this collection.
     * 
     * @return {Object} 
     * @private
     */
    aggregateGroups: function(groups, property, operation, scope) {
        var items = groups.items,
            len  = items.length,
            callDirect = !Ext.isFunction(operation),
            out = {},
            i, group, result;
        
        for (= 0; i < len; ++i) {
            group = items[i];
            if (!callDirect) {
                result = this.aggregateItems(group.items, property, operation, null, null, scope);
            } else {
                result = group[operation](property);
            }
            out[group.getGroupKey()] = result;
        }
        return out;
    },
 
    /**
     * This method is called to indicate the start of multiple changes to the collection.
     * Application code should seldom need to call this method as it is called internally
     * when needed. If multiple collection changes are needed, consider wrapping them in
     * an `update` call rather than calling `beginUpdate` directly.
     *
     * Internally this method increments a counter that is decremented by `endUpdate`. It
     * is important, therefore, that if you call `beginUpdate` directly you match that
     * call with a call to `endUpdate` or you will prevent the collection from updating
     * properly.
     *
     * For example:
     *
     *      var collection = new Ext.util.Collection();
     *
     *      collection.beginUpdate();
     *
     *      collection.add(item);
     *      // ...
     *
     *      collection.insert(index, otherItem);
     *      //...
     *
     *      collection.endUpdate();
     *
     * @since 5.0.0
     */
    beginUpdate: function () {
        if (!this.updating++) { // jshint ignore:line
            this.notify('beginupdate');
        }
    },
 
    /**
     * Removes all items from the collection. This is similar to `removeAll` except that
     * `removeAll` fire events to inform listeners. This means that this method should be
     * called only when you are sure there are no listeners.
     * @since 5.0.0
     */
    clear: function () {
        var me = this,
            generation = me.generation,
            ret = generation ? me.items : [],
            extraKeys,
            indexName;
 
        if (generation) {
            me.items.length = me.length = 0;
            me.map = {};
            me.indices = {};
            me.generation++;
 
            // Clear any extraKey indices associated with this Collection
            extraKeys = me.getExtraKeys();
            if (extraKeys) {
                for (indexName in extraKeys) {
                    extraKeys[indexName].clear();
                }
            }
        }
 
        return ret;
    },
 
    /**
     * Creates a shallow copy of this collection
     * @return {Ext.util.Collection} 
     * @since 5.0.0
     */
    clone: function () {
        var me = this,
            copy = new me.self(me.initialConfig);
 
        copy.add(me.items);
        return copy;
    },
 
    /**
     * Collects unique values of a particular property in this Collection.
     * @param {String} property The property to collect on
     * @param {String} root (optional) 'root' property to extract the first argument from. This is used mainly when
     * summing fields in records, where the fields are all stored inside the 'data' object
     * @param {Boolean} [allowNull] Pass `true` to include `null`, `undefined` or empty
     * string values.
     * @return {Array} The unique values
     * @since 5.0.0
     */
    collect: function (property, root, allowNull) {
        var items = this.items,
            length = items.length,
            map = {},
            ret = [],
            i, strValue, value;
 
        for (= 0; i < length; ++i) {
            value = items[i];
            value = (root ? value[root] : value)[property];
 
            strValue = String(value);
 
            if ((allowNull || !Ext.isEmpty(value)) && !map[strValue]) {
                map[strValue] = 1;
                ret.push(value);
            }
        }
 
        return ret;
    },
 
    /**
     * Returns true if the collection contains the passed Object as an item.
     * @param {Object} item The item to look for in the collection.
     * @return {Boolean} `true` if the collection contains the item.
     * @since 5.0.0
     */
    contains: function (item) {
        var ret = false,
            key;
 
        if (item != null) {
            key = this.getKey(item);
            ret = this.map[key] === item;
        }
 
        return ret;
    },
 
    /**
     * Returns true if the collection contains all the passed items. If the first argument
     * is an array, then the items in that array are checked. Otherwise, all arguments
     * passed to this method are checked.
     *
     * @param {Object.../Object[]} items The item(s) that must be in the collection.
     * @return {Boolean} `true` if the collection contains all the items.
     * @since 6.5.2
     */
    containsAll: function (items) {
        var all = Ext.isArray(items) ? items : arguments,
            i;
 
        for (= all.length; i-- > 0; ) {
            if (!this.contains(all[i])) {
                return false;
            }
        }
 
        return true;
    },
 
    /**
     * Returns true if the collection contains the passed Object as a key.
     * @param {String} key The key to look for in the collection.
     * @return {Boolean} True if the collection contains the Object as a key.
     * @since 5.0.0
     */
    containsKey: function (key) {
        return key in this.map;
    },
 
    /**
     * Creates a new collection that is a filtered subset of this collection. The filter
     * passed can be a function, a simple property name and value, an `Ext.util.Filter`
     * instance, an array of `Ext.util.Filter` instances.
     *
     * If the passed filter is a function the second argument is its "scope" (or "this"
     * pointer). The function should return `true` given each item in the collection if
     * that item should be included in the filtered collection.
     *
     *      var people = new Ext.util.Collection();
     *
     *      people.add([
     *          { id: 1, age: 25, name: 'Ed' },
     *          { id: 2, age: 24, name: 'Tommy' },
     *          { id: 3, age: 24, name: 'Arne' },
     *          { id: 4, age: 26, name: 'Aaron' }
     *      ]);
     *
     *      // Create a collection of people who are older than 24:
     *      var oldPeople = people.createFiltered(function (item) {
     *          return item.age > 24;
     *      });
     *
     * If the passed filter is a `Ext.util.Filter` instance or array of `Ext.util.Filter`
     * instances the filter(s) are used to produce the filtered collection and there are
     * no further arguments.
     *
     * If the passed filter is a string it is understood as the name of the property by
     * which to filter. The second argument is the "value" used to compare each item's
     * property value. This comparison can be further tuned with the `anyMatch` and
     * `caseSensitive` (optional) arguments.
     *
     *     // Create a new Collection containing only the items where age == 24
     *     var middleAged = people.createFiltered('age', 24);
     *
     * Alternatively you can apply `filters` to this Collection by calling `setFilters`
     * or modifying the filter collection returned by `getFilters`.
     *
     * @param {Ext.util.Filter[]/String/Function} property A property on your objects, an
     * array of {@link Ext.util.Filter Filter} objects or a filter function.
     *
     * @param {Object} value If `property` is a function, this argument is the "scope"
     * (or "this" pointer) for the function. Otherwise this is either a `RegExp` to test
     * property values or the value with which to compare.
     *
     * @param {Boolean} [anyMatch=false] True to match any part of the string, not just
     * the beginning.
     *
     * @param {Boolean} [caseSensitive=false] True for case sensitive comparison.
     *
     * @param {Boolean} [exactMatch=false] `true` to force exact match (^ and $ characters added to the regex).
     *
     * @return {Ext.util.Collection} The new, filtered collection.
     *
     * @since 5.0.0
     */
    createFiltered: function (property, value, anyMatch, caseSensitive, exactMatch) {
        var me = this,
            ret = new me.self(me.initialConfig),
            root = me.getRootProperty(),
            items = me.items,
            length, i, filters, fn, scope;
 
        if (Ext.isFunction(property)) {
            fn = property;
            scope = value;
        } else {
            //support for the simple case of filtering by property/value
            if (Ext.isString(property)) {
                filters = [
                    new Ext.util.Filter({
                        property     : property,
                        value        : value,
                        root         : root,
                        anyMatch     : anyMatch,
                        caseSensitive: caseSensitive,
                        exactMatch   : exactMatch
                    })
                ];
            } else if (property instanceof Ext.util.Filter) {
                filters = [ property ];
                property.setRoot(root);
            } else if (Ext.isArray(property)) {
                filters = property.slice(0);
                for (= 0, length = filters.length; i < length; ++i) {
                    filters[i].setRoot(root);
                }
            }
 
            // At this point we have an array of zero or more Ext.util.Filter objects to
            // filter with, so here we construct a function that combines these filters by
            // ANDing them together and filter by that.
            fn = Ext.util.Filter.createFilterFn(filters);
        }
 
        scope = scope || me;
 
        for (= 0, length = items.length; i < length; i++) {
            if (fn.call(scope, items[i])) {
                ret.add(items[i]);
            }
        }
 
        return ret;
    },
 
    /**
     * Filter by a function. Returns a <i>new</i> collection that has been filtered.
     * The passed function will be called with each object in the collection.
     * If the function returns true, the value is included otherwise it is filtered.
     * @param {Function} fn The function to be called.
     * @param {Mixed} fn.item The collection item.
     * @param {String} fn.key The key of collection item.
     * @param {Object} scope (optional) The scope (<code>this</code> reference) in
     * which the function is executed. Defaults to this Collection.
     * @return {Ext.util.Collection} The new filtered collection
     * @deprecated 5.0.0 This method is deprecated.
     */
    filterBy: function(fn, scope) {
        return this.createFiltered(fn, scope);
    },
 
    /**
     * Executes the specified function once for every item in the collection. If the value
     * returned by `fn` is `false` the iteration stops. In all cases, the last value that
     * `fn` returns is returned by this method.
     *
     * @param {Function} fn The function to execute for each item.
     * @param {Object} fn.item The collection item.
     * @param {Number} fn.index The index of item.
     * @param {Number} fn.len Total length of collection.
     * @param {Object} [scope=this] The scope (`this` reference) in which the function
     * is executed. Defaults to this collection.
     * @since 5.0.0
     */
    each: function (fn, scope) {
        var items = this.items,
            len = items.length,
            i, ret;
 
        if (len) {
            scope = scope || this;
            items = items.slice(0); // safe for re-entrant calls
 
            for (= 0; i < len; i++) {
                ret = fn.call(scope, items[i], i, len);
                if (ret === false) {
                    break;
                }
            }
        }
 
        return ret;
    },
 
    /**
     * Executes the specified function once for every key in the collection, passing each
     * key, and its associated item as the first two parameters. If the value returned by
     * `fn` is `false` the iteration stops. In all cases, the last value that `fn` returns
     * is returned by this method.
     *
     * @param {Function} fn The function to execute for each item.
     * @param {String} fn.key The key of collection item.
     * @param {Object} fn.item The collection item.
     * @param {Number} fn.index The index of item.
     * @param {Number} fn.len Total length of collection.
     * @param {Object} [scope=this] The scope (`this` reference) in which the function
     * is executed. Defaults to this collection.
     * @since 5.0.0
     */
    eachKey: function (fn, scope) {
        var me = this,
            items = me.items,
            len = items.length,
            i, item, key, ret;
 
        if (len) {
            scope = scope || me;
            items = items.slice(0); // safe for re-entrant calls
 
            for (= 0; i < len; i++) {
                key = me.getKey(item = items[i]);
                ret = fn.call(scope, key, item, i, len);
                if (ret === false) {
                    break;
                }
            }
        }
 
        return ret;
    },
 
    /**
     * This method is called after modifications are complete on a collection. For details
     * see `beginUpdate`.
     * @since 5.0.0
     */
    endUpdate: function () {
        if (! --this.updating) {
            this.notify('endupdate');
        }
    },
 
    /**
     * Finds the first matching object in this collection by a specific property/value.
     *
     * @param {String} property The name of a property on your objects.
     * @param {String/RegExp} value A string that the property values
     * should start with or a RegExp to test against the property.
     * @param {Number} [start=0] The index to start searching at.
     * @param {Boolean} [startsWith=true] Pass `false` to allow a match start anywhere in
     * the string. By default the `value` will match only at the start of the string.
     * @param {Boolean} [endsWith=true] Pass `false` to allow the match to end before the
     * end of the string. By default the `value` will match only at the end of the string.
     * @param {Boolean} [ignoreCase=true] Pass `false` to make the `RegExp` case
     * sensitive (removes the 'i' flag).
     * @return {Object} The first item in the collection which matches the criteria or
     * `null` if none was found.
     * @since 5.0.0
     */
    find: function (property, value, start, startsWith, endsWith, ignoreCase) {
        if (Ext.isEmpty(value, false)) {
            return null;
        }
 
        var regex = Ext.String.createRegex(value, startsWith, endsWith, ignoreCase),
            root = this.getRootProperty();
 
        return this.findBy(function (item) {
            return item && regex.test((root ? item[root] : item)[property]);
        }, null, start);
    },
 
    /**
     * Returns the first item in the collection which elicits a true return value from the
     * passed selection function.
     * @param {Function} fn The selection function to execute for each item.
     * @param {Object} fn.item The collection item.
     * @param {String} fn.key The key of collection item.
     * @param {Object} [scope=this] The scope (`this` reference) in which the function
     * is executed. Defaults to this collection.
     * @param {Number} [start=0] The index at which to start searching.
     * @return {Object} The first item in the collection which returned true from the selection
     * function, or null if none was found.
     * @since 5.0.0
     */
    findBy: function (fn, scope, start) {
        var me = this,
            items = me.items,
            len = items.length,
            i, item, key;
 
        scope = scope || me;
        for (= start || 0; i < len; i++) {
            key = me.getKey(item = items[i]);
            if (fn.call(scope, item, key)) {
                return items[i];
            }
        }
 
        return null;
    },
 
    /**
     * Finds the index of the first matching object in this collection by a specific
     * property/value.
     *
     * @param {String} property The name of a property on your objects.
     * @param {String/RegExp} value A string that the property values
     * should start with or a RegExp to test against the property.
     * @param {Number} [start=0] The index to start searching at.
     * @param {Boolean} [startsWith=true] Pass `false` to allow a match start anywhere in
     * the string. By default the `value` will match only at the start of the string.
     * @param {Boolean} [endsWith=true] Pass `false` to allow the match to end before the
     * end of the string. By default the `value` will match only at the end of the string.
     * @param {Boolean} [ignoreCase=true] Pass `false` to make the `RegExp` case
     * sensitive (removes the 'i' flag).
     * @return {Number} The matched index or -1 if not found.
     * @since 5.0.0
     */
    findIndex: function (property, value, start, startsWith, endsWith, ignoreCase) {
        var item = this.find(property, value, start, startsWith, endsWith, ignoreCase);
 
        return item ? this.indexOf(item) : -1;
    },
 
    /**
     * Find the index of the first matching object in this collection by a function.
     * If the function returns `true` it is considered a match.
     * @param {Function} fn The function to be called.
     * @param {Object} fn.item The collection item.
     * @param {String} fn.key The key of collection item.
     * @param {Object} [scope=this] The scope (`this` reference) in which the function
     * is executed. Defaults to this collection.
     * @param {Number} [start=0] The index at which to start searching.
     * @return {Number} The matched index or -1
     * @since 5.0.0
     */
    findIndexBy: function (fn, scope, start) {
        var item = this.findBy(fn, scope, start);
        return item ? this.indexOf(item) : -1;
    },
 
    /**
     * Returns the first item in the collection.
     * @param {Boolean} [grouped] `true` to extract the first item in each group. Only applies if
     * a {@link #grouper} is active in the collection.
     * @return {Object} The first item in the collection. If the grouped parameter is passed,
     * see {@link #aggregateByGroup} for information on the return type.
     * @since 5.0.0
     */
    first: function (grouped) {
        var groups = grouped ? this.getGroups() : undefined;
        return groups ? this.aggregateGroups(groups, null, 'first') : this.items[0];
    },
    
    /**
     * Returns the last item in the collection.
     * @param {Boolean} [grouped] `true` to extract the first item in each group. Only applies if
     * a {@link #grouper} is active in the collection.
     * @return {Object} The last item in the collection. If the grouped parameter is passed,
     * see {@link #aggregateByGroup} for information on the return type.
     * @since 5.0.0
     */
    last: function (grouped) {
        var groups = grouped ? this.getGroups() : undefined;
        return groups ? this.aggregateGroups(groups, null, 'last') : this.items[this.length - 1];
    },
 
    /**
     * Returns the item associated with the passed key.
     * @param {String/Number} key The key of the item.
     * @return {Object} The item associated with the passed key.
     * @since 5.0.0
     */
    get: function (key) {
        return this.map[key];
    },
 
    /**
     * Returns the item at the specified index.
     * @param {Number} index The index of the item.
     * @return {Object} The item at the specified index.
     * @since 5.0.0
     */
    getAt: function (index) {
        return this.items[index];
    },
 
    /**
     * Returns the item associated with the passed key.
     * @param {String/Number} key The key of the item.
     * @return {Object} The item associated with the passed key.
     * @since 5.0.0
     */
    getByKey: function (key) {
        return this.map[key];
    },
 
    /**
     * Returns the number of items in the collection.
     * @return {Number} the number of items in the collection.
     * @since 5.0.0
     */
    getCount: function () {
        return this.length;
    },
 
    /**
     * A function which will be called, passing an object belonging to this collection.
     * The function should return the key by which that object will be indexed. This key
     * must be unique to this item as only one item with this key will be retained.
     *
     * The default implementation looks basically like this (give or take special case
     * handling of 0):
     *
     *      function getKey (item) {
     *          return item.id || item._id || item.getId();
     *      }
     *
     * You can provide your own implementation by passing the `keyFn` config.
     *
     * For example, to hold items that have a unique "name" property:
     *
     *     var elementCollection = new Ext.util.Collection({
     *         keyFn: function (item) {
     *             return item.name;
     *         }
     *     });
     *
     * The collection can have `extraKeys` if items need to be quickly looked up by other
     * (potentially non-unique) properties.
     *
     * @param {Object} item The item.
     * @return {Object} The key for the passed item.
     * @since 5.0.0
     */
    getKey: function (item) {
        var id = item.id;
        return (id === 0 || id) ? id :
              ((id = item._id) === 0 || id) ? id : item.getId();
    },
 
    /**
     * Returns a range of items in this collection
     * @param {Number} [begin=0] The index of the first item to get.
     * @param {Number} [end] The ending index. The item at this index is *not* included.
     * @return {Array} An array of items
     * @since 5.0.0
     */
    getRange: function (begin, end) {
        var items = this.items,
            length = items.length,
            range;
 
        //<debug>
        if (begin > end) {
            Ext.raise('Inverted range passed to Collection.getRange: [' + begin +
                            ',' + end + ']');
        }
        //</debug>
 
        if (!length) {
            range = [];
        } else {
            range = Ext.Number.clipIndices(length, [begin, end]);
            range = items.slice(range[0], range[1]);
        }
 
        return range;
    },
    
    /**
     * @method getSource
     * Returns all unfiltered items in the Collection when the Collection has been 
     * filtered.  Returns `null` when the Collection is not filtered.
     * @return {Ext.util.Collection} items All unfiltered items (or `null` when the 
     * Collection is not filtered)
     */
 
    /**
     * Returns an array of values for the specified (sub) property.
     *
     * For example, to get an array of "name" properties from a collection of records (of
     * `Ext.data.Model` objects):
     *
     *      var names = collection.getValues('name', 'data');
     *
     * @param {String} property The property to collect on
     * @param {String} [root] 'root' property to extract the first argument from. This is
     * used mainly when operating on fields in records, where the fields are all stored
     * inside the 'data' object.
     * @param {Number} [start=0] The index of the first item to include.
     * @param {Number} [end] The index at which to stop getting values. The value of this
     * item is *not* included.
     * @return {Object[]} The array of values.
     * @since 5.0.0
     */
    getValues: function (property, root, start, end) {
        var items = this.items,
            range = Ext.Number.clipIndices(items.length, [start, end]),
            ret = [],
            i, value;
 
        for (= range[0], end = range[1]; i < end; ++i) {
            value = items[i];
            value = (root ? value[root] : value)[property];
            ret.push(value);
        }
 
        return ret;
    },
 
    /**
     * Returns index within the collection of the passed Object.
     * @param {Object} item The item to find.
     * @return {Number} The index of the item or -1 if not found.
     * @since 5.0.0
     */
    indexOf: function (item) {
        if (!item) {
            return -1;
        }
        
        var key = this.getKey(item);
        return this.indexOfKey(key);
    },
 
    /**
     * Returns index within the collection of the passed key.
     * @param {Object} key The key to find.
     * @return {Number} The index of the item or -1 if not found.
     * @since 5.0.0
     */
    indexOfKey: function (key) {
        var me = this,
            indices = me.indices;
 
        if (key in me.map) {
            if (!indices) {
                indices = me.getIndices();
            }
            return indices[key];
        }
 
        return -1;
    },
 
    /**
     * Inserts one or more items to the collection. The `index` value is the position at
     * which the first item will be placed. The items starting at that position will be
     * shifted to make room.
     *
     * @param {Number} index The index at which to insert the item(s).
     * @param {Object/Object[]} item The item or items to add.
     * @return {Object/Object[]} The item or items added.
     * @since 5.0.0
     */
    insert: function (index, item) {
        var me = this,
            items = me.decodeItems(arguments, 1),
            ret = items;
 
        if (items.length) {
            me.splice(index, 0, items);
            ret = (items.length === 1) ? items[0] : items;
        }
 
        return ret;
    },
 
    /**
     * This method should be called when an item in this collection has been modified. If
     * the collection is sorted or filtered the result of modifying an item needs to be
     * reflected in the collection. If the item's key is also being modified, it is best
     * to pass the `oldKey` to this same call rather than call `updateKey` separately.
     *
     * @param {Object} item The item that was modified.
     * @param {String[]} [modified] The names of the modified properties of the item.
     * @param {String/Number} [oldKey] Passed if the item's key was also modified.
     * @param {Object} meta (private)
     * @since 5.0.0
     */
    itemChanged: function (item, modified, oldKey, meta) {
        var me = this,
            keyChanged = oldKey !== undefined,
            filtered = me.filtered && me.getAutoFilter(),
            filterChanged = false,
            itemMovement = 0,
            items = me.items,
            last = me.length - 1,
            sorted = me.sorted && last > 0 && me.getAutoSort(), // one or zero items is not really sorted
                                                                // CAN be called on an empty Collection
                                                                // A TreeStore can call afterEdit on a hidden root before
                                                                // any child nodes exist in the store.
            source = me.getSource(),
            toRemove = 0,
            itemFiltered = false,
            wasFiltered = false,
            details, newKey, sortFn,
            toAdd, index, newIndex;
 
        // We are owned, we cannot react, inform owning collection.
        if (source && !source.updating) {
            me.sourceUpdating = true;
            source.itemChanged(item, modified, oldKey, meta);
            me.sourceUpdating = false;
        }
 
        // Root Collection has been informed.
        // Change is propagating downward from root.
        else {
            newKey = me.getKey(item);
 
            if (filtered) {
                index = me.indexOfKey(keyChanged ? oldKey : newKey);
                wasFiltered = (index < 0);
                itemFiltered = me.isItemFiltered(item);
                filterChanged = (wasFiltered !== itemFiltered);
            }
 
            if (filterChanged) {
                if (itemFiltered) {
                    toRemove = [ item ];
                    newIndex = -1;
                } else {
                    toAdd = [ item ];
                    newIndex = me.length; // this will be ignored if sorted
                }
            }
            // If sorted, the newIndex must be reported correctly in the beforeitemchange and itemchange events.
            // Even though splice ignores the parameter and calculates the insertion point
            else if (sorted && !itemFiltered) {
                // If we are sorted and there are 2 or more items make sure this item is at
                // the proper index.
                if (!filtered) {
                    // If the filter has not changed we may need to move the item but if
                    // there is a filter we have already determined its index.
                    index = me.indexOfKey(keyChanged ? oldKey : newKey);
                }
 
                sortFn = me.getSortFn();
 
                if (index !== -1) {
                    if (index && sortFn(items[index - 1], items[index]) > 0) {
                        // If this item is not the first and the item before it compares as
                        // greater-than then item needs to move left since it is less-than
                        // item[index - 1].
                        itemMovement = -1;
 
                        // We have to bound the binarySearch or else the presence of the
                        // out-of-order "item" would break it.
                        newIndex = Ext.Array.binarySearch(items, item, 0, index, sortFn);
                    } else if (index < last && sortFn(items[index], items[index + 1]) > 0) {
                        // If this item is not the last and the item after it compares as
                        // less-than then item needs to move right since it is greater-than
                        // item[index + 1].
                        itemMovement = 1;
 
                        // We have to bound the binarySearch or else the presence of the
                        // out-of-order "item" would break it.
                        newIndex = Ext.Array.binarySearch(items, item, index + 1, sortFn);
                    }
 
                    if (itemMovement) {
                        toAdd = [ item ];
                    }
                }
            }
 
            // One may be tempted to avoid this notification when none of our three vars
            // are true, *but* the problem with that is that these three changes we care
            // about are only what this collection cares about. Child collections or
            // outside parties still need to know that the item has changed in some way.
            // We do NOT adjust the newIndex reported here to allow for position *after* the item has been removed
            // We report the "visual" position at which the item would be inserted as if it were new.
            details = {
                item: item,
                key: newKey,
                index: newIndex,
 
                filterChanged: filterChanged,
                keyChanged: keyChanged,
                indexChanged: !!itemMovement,
 
                filtered: itemFiltered,
                oldIndex: index,
                newIndex: newIndex,
                wasFiltered: wasFiltered,
                meta: meta
            };
 
            if (keyChanged) {
                details.oldKey = oldKey;
            }
            if (modified) {
                details.modified = modified;
            }
 
            ++me.generation;
 
            me.beginUpdate();
 
            me.notify('beforeitemchange', [details]);
 
            if (keyChanged) {
                me.updateKey(item, oldKey, details);
            }
 
            if (toAdd || toRemove) {
                // In sorted mode (which is the only time we get here), newIndex is
                // correct but *ignored* by splice since it has to assume that *insert*
                // index values need to be determined internally. In other words, the
                // first argument here is both the remove and insert index but in sorted
                // mode the insert index is calculated by splice.
                me.splice(newIndex, toRemove, toAdd);
            }
 
            // Ensure that the newIndex always refers to the item the insertion is *before*.
            // Ensure that the oldIndex always refers to the item the insertion was *before*.
            //
            // Before change to "c" to "h":     |   Before change "i" to "d":
            //                                  |
            //      +---+---+---+---+---+---+   |   +---+---+---+---+---+---+
            //      | a | c | e | g | i | k |   |   | a | c | e | g | i | k |
            //      +---+---+---+---+---+---+   |   +---+---+---+---+---+---+
            //        0   1   2   3   4   5     |     0   1   2   3   4   5   
            //            ^           ^         |             ^       ^
            //            |           |         |             |       |
            //        oldIndex    newIndex      |       newIndex     oldIndex
            //                                  |
            // After change to "c" to "h":      |   After change "i" to "d":
            //                                  |
            //      +---+---+---+---+---+---+   |   +---+---+---+---+---+---+
            //      | a | e | g | h | i | k |   |   | a | c | d | e | g | k |
            //      +---+---+---+---+---+---+   |   +---+---+---+---+---+---+
            //        0   1   2   3   4   5     |     0   1   2   3   4   5  
            //            ^       ^             |             ^           ^
            //            |       |             |             |           |
            //      oldIndex    newIndex        |        newIndex     oldIndex
            //
            if (itemMovement > 0) {
                details.newIndex--;
            } else if (itemMovement < 0) {
                details.oldIndex++;
            }
 
            // Divergence depending on whether the record if filtered out at this level in a chaining hierarchy.
            // Child collections of this collection will not care about filtereditemchange because the record is not in them.
            // Stores however will still need to know because the record *is* in them, just filtered.
            me.notify(itemFiltered ? 'filtereditemchange' : 'itemchange', [details]);
 
            me.endUpdate();
        }
    },
 
    /**
     * Remove an item from the collection.
     * @param {Object/Object[]} item The item or items to remove.
     * @return {Number} The number of items removed.
     * @since 5.0.0
     */
    remove: function (item) {
        var me = this,
            items = me.decodeRemoveItems(arguments, 0),
            length = me.length;
 
        me.splice(0, items);
 
        return length - me.length;
    },
 
    /**
     * Remove all items in the collection.
     * @return {Ext.util.Collection} This object.
     * @since 5.0.0
     */
    removeAll: function () {
        var me = this,
            length = me.length;
 
        if (me.generation && length) {
            me.splice(0, length);
        }
 
        return me;
    },
 
    /**
     * Remove an item from a specified index in the collection.
     * @param {Number} index The index within the collection of the item to remove.
     * @param {Number} [count=1] The number of items to remove.
     * @return {Object/Number} If `count` was 1 and the item was removed, that item is
     * returned. Otherwise the number of items removed is returned.
     * @since 5.0.0
     */
    removeAt: function (index, count) {
        var me = this,
            length = me.length,
            Num = Ext.Number,
            range = Num.clipIndices(length, [ index, (count === undefined) ? 1 : count ],
                                    Num.Clip.COUNT),
            n = range[0],
            removeCount = range[1] - n,
            item = (removeCount === 1) && me.getAt(n),
            removed;
 
        me.splice(n, removeCount);
 
        removed = me.length - length;
 
        return (item && removed) ? item : removed;
    },
 
    /**
     * Removes the item associated with the passed key from the collection.
     * @param {String} key The key of the item to remove.
     * @return {Object} Only returned if removing at a specified key. The item removed or
     * `false` if no item was removed.
     * @since 5.0.0
     */
    removeByKey: function (key) {
        var item = this.getByKey(key);
 
        if (!item || !this.remove(item)) {
            return false;
        }
 
        return item;
    },
 
    /**
     * @private
     * Replace an old entry with a new entry of the same key if the entry existed.
     * @param {Object} item The item to insert.
     * @return {Object} inserted The item inserted.
     */
    replace: function(item) {
        var index = this.indexOf(item);
 
        if (index === -1) {
            this.add(item);
        } else {
            this.insert(index, item);
        }
    },
 
    /**
     * 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`.
     * @since 5.0.0
     */
    splice: function (index, toRemove, toAdd) {
        var me = this,
            autoSort = me.sorted && me.getAutoSort(),
            map = me.map,
            items = me.items,
            length = me.length,
            removeItems = (toRemove instanceof Array) ? me.decodeRemoveItems(toRemove) : null,
            isRemoveCount = !removeItems,
            Num = Ext.Number,
            range = Num.clipIndices(length, [index, isRemoveCount ? toRemove : 0],
                                    Num.Clip.COUNT),
            begin = range[0],
            end = range[1],
            // Determine how many items we might actually remove:
            removeCount = end - begin,
            newItems = me.decodeItems(arguments, 2),
            newCount = newItems ? newItems.length : 0,
            addItems, newItemsMap, removeMap,
            insertAt = begin,
            indices = me.indices || ((newCount || removeItems) ? me.getIndices() : null),
            adds = null,
            removes = removeCount ? [begin] : null,
            newKeys = null,
            source = me.getSource(),
            chunk, chunkItems, chunks, i, item, itemIndex, k, key, keys, n, duplicates,
            sorters;
 
        if (source && !source.updating) {
            // Modifying the content of a child collection has to be translated into a
            // change of its source. Because the source has all of the items of the child
            // (but likely at different indices) we can translate "index" and convert a
            // "removeCount" request into a "removeItems" request.
            if (isRemoveCount) {
                removeItems = [];
                for (= 0; i < removeCount; ++i) {
                    removeItems.push(items[begin + i]);
                }
            }
 
            if (begin < length) {
                // Map index based on the item at that index since that item will be in
                // the source collection.
                i = source.indexOf(items[begin]);
            } else {
                // Map end of this collection to end of the source collection.
                i = source.length;
            }
 
            // When we react to the source add in onCollectionAdd, we must honour this requested index.
            me.requestedIndex = index;
            source.splice(i, removeItems, newItems);
            delete me.requestedIndex;
            return me;
        }
 
        // Loop over the newItems because they could already be in the collection or may
        // be replacing items in the collection that just happen to have the same key. In
        // this case, those items must be removed as well. Since we need to call getKey
        // on each newItem to do this we may as well keep those keys for later.
        if (newCount) {
            addItems = newItems;
            newKeys = [];
            newItemsMap = {};
 
            // If this collection is sorted we will eventually need to sort addItems so
            // do that now so we can line up the newKeys properly. We optimize for the
            // case where we have no duplicates. It would be more expensive to do this
            // in two passes in an attempt to take advantage of removed duplicates.
            if (autoSort) {
                // We'll need the sorters later as well
                sorters = me.getSorters();
 
                if (newCount > 1) {
                    if (!addItems.$cloned) {
                        newItems = addItems = addItems.slice(0);
                    }
                    me.sortData(addItems);
                }
            }
 
            for (= 0; i < newCount; ++i) {
                key = me.getKey(item = newItems[i]);
 
                if ((= newItemsMap[key]) !== undefined) {
                    // Duplicates in the incoming newItems need to be discarded keeping the
                    // last of the duplicates. We add the index of the last duplicate of
                    // this key to the "duplicates" map.
                    (duplicates || (duplicates = {}))[k] = 1;
                } else {
                    // This item's index is outside the remove range, so we need to remove
                    // some extra stuff. Only the first occurrence of a given key in the
                    // newItems needs this processing.
                    itemIndex = indices[key];
                    if (itemIndex < begin || end <= itemIndex) {
                        (removes || (removes = [])).push(itemIndex); // might be the first
                    }
                }
 
                newItemsMap[key] = i; // track the last index of this key in newItems
                newKeys.push(key); // must correspond 1-to-1 with newItems
            }
 
            if (duplicates) {
                keys = newKeys;
                addItems = [];
                newKeys = [];
                addItems.$cloned = true;
 
                for (= 0; i < newCount; ++i) {
                    if (!duplicates[i]) {
                        item = newItems[i];
                        addItems.push(item);
                        newKeys.push(keys[i]);
                    }
                }
 
                newCount = addItems.length;
            }
 
            adds = {
                //at: insertAt, // must fill this in later
                //next: null,  // only set by spliceMerge
                //replaced: null,  // must fill this in later
                items: addItems,
                keys: newKeys
            };
        }
 
        // If we are given a set of items to remove, map them to their indices.
        for (= removeItems ? removeItems.length : 0; i-- > 0; ) {
            key = me.getKey(removeItems[i]);
            if ((itemIndex = indices[key]) !== undefined) {
                // ignore items we don't have (probably due to filtering)
                (removes || (removes = [])).push(itemIndex); // might be the first remove
            }
        }
 
        if (!adds && !removes) {
            return me;
        }
 
        me.beginUpdate();
 
        // Now we that everything we need to remove has its index in the removes array.
        // We start by sorting the array so we can coalesce the index values into chunks
        // or ranges.
        if (removes) {
            chunk = null;
            chunks = [];
            removeMap = {};
            if (removes.length > 1) {
                removes.sort(Ext.Array.numericSortFn);
            }
 
            // Coalesce the index array into chunks of (index, count) pairs for efficient
            // removal.
            for (= 0, n = removes.length; i < n; ++i) {
                key = me.getKey(item = items[itemIndex = removes[i]]);
                if (!(key in map)) {
                    continue;
                }
 
                // Avoids 2nd loop of removed items but also means we won't process any
                // given item twice (in case of duplicates in removeItems).
                delete map[key];
 
                // Consider chunk = { at: 1, items: [ item1, item2 ] }
                //
                //      +---+---+---+---+---+---+
                //      |   | x | x |   |   |   |
                //      +---+---+---+---+---+---+
                //        0   1   2   3   4   5
                //
                // If we are adding an itemIndex > 3 we need a new chunk.
                //
                if (!chunk || itemIndex > (chunk.at + chunkItems.length)) {
                    chunks.push(chunk = {
                        at: itemIndex,
                        items: (chunkItems = []),
                        keys: (keys = []),
                        map: removeMap,
                        next: chunk,
                        replacement: adds
                    });
 
                    // Point "replaced" at the last chunk
                    if (adds) {
                        adds.replaced = chunk;
                    }
                }
 
                chunkItems.push(removeMap[key] = item);
                keys.push(key);
 
                if (itemIndex < insertAt - 1) {
                    // If the removal is ahead of the insertion point specified, we need
                    // to move the insertAt backwards.
                    //
                    // Consider the following splice:
                    //
                    //      collection.splice(3, 2, [ { id: 'b' } ]);
                    //
                    //      +---+---+---+---+---+---+
                    //      | a | b | c | x | y | d |
                    //      +---+---+---+---+---+---+
                    //        0   1   2   3   4   5
                    //            ^       ^   ^
                    //            |        \ /
                    //         replace    remove
                    //
                    // The intent is to replace x and y with the new item at index=3. But
                    // since the new item has the same key as the item at index=1, that
                    // item must be replaced. The resulting collection will be:
                    //
                    //      +---+---+---+---+
                    //      | a | c | b | d |
                    //      +---+---+---+---+
                    //        0   1   2   3
                    //
                    --insertAt;
                }
 
                if (removeCount > 1 && itemIndex === begin) {
                    // To account for the given range to remove we started by putting the
                    // index of the first such item ("begin") in the array. When we find
                    // it in this loop we have to process all of the items and add them
                    // to the current chunk. The following trick allows us to repeat the
                    // loop for each item in the removeCount.
                    //
                    --removeCount; // countdown...
                    removes[i--] = ++begin; // backup and increment begin
                }
            } // for (removes)
 
            if (adds) {
                adds.at = insertAt; // we have the correct(ed) insertAt now
            }
 
            // Loop over the chunks in reverse so as to not invalidate index values on
            // earlier chunks.
            for (= chunks.length; k-- > 0; ) {
                chunk = chunks[k];
                i = chunk.at;
                n = chunk.items.length;
 
                if (+ n < length) {
                    // If we are removing the tail of the collection, we can keep the
                    // indices for the rest of the things... otherwise we need to zap it
                    // and fix up later.
                    me.indices = indices = null;
                }
 
                me.length = length -= n;
                // We can use splice directly. The IE8 bug which Ext.Array works around
                // only affects *insertion*
                // http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/6e946d03-e09f-4b22-a4dd-cd5e276bf05a/
                //Ext.Array.erase(items, i, n);
                items.splice(i, n);
 
                if (indices) {
                    keys = chunk.keys;
                    for (= 0; i < n; ++i) {
                        delete indices[keys[i]];
                    }
                }
 
                ++me.generation;
                me.notify('remove', [ chunk ]);
            }
        } // if (removes)
 
        if (adds) {
            if (autoSort && newCount > 1 && length) {
                me.spliceMerge(addItems, newKeys);
            } else {
                if (autoSort) {