/**
 * @class Ext.data.PageMap
 * @extends Ext.util.LruCache
 * Private class for use by only Store when configured `buffered: true`.
 * @private
 */
Ext.define('Ext.data.PageMap', {
    extend: 'Ext.util.LruCache',

    // Maintain a generation counter, so that the Store can reject incoming pages destined for the previous generation
    clear: function(initial) {
        var me = this;
        me.pageMapGeneration = (me.pageMapGeneration || 0) + 1;
        me.callParent(arguments);
    },

    forEach: function(fn, scope) {
        var me = this,
            pageNumbers = Ext.Object.getKeys(me.map),
            pageCount = pageNumbers.length,
            i, j,
            pageNumber,
            page,
            pageSize;

        for (i = 0; i < pageCount; i++) {
            pageNumbers[i] = Number(pageNumbers[i]);
        }
        Ext.Array.sort(pageNumbers);
        scope = scope || me;
        for (i = 0; i < pageCount; i++) {
            pageNumber = pageNumbers[i];
            page = me.getPage(pageNumber);
            pageSize = page.length;
            for (j = 0; j < pageSize; j++) {
                if (fn.call(scope, page[j], (pageNumber - 1) * me.pageSize + j) === false) {
                    return;
                }
            }
        }
    },

    /**
    * Returns the first record in this page map which elicits a true return value from the
    * passed selection function.
    *
    * **IMPORTANT
    * This can ONLY find records which happen to be cached in the page cache. This will be parts of the dataset around the currently
    * visible zone, or recently visited zones if the pages have not yet been purged from the cache.
    * 
    * This CAN NOT find records which have not been loaded into the cache.**
    *
    * If full client side searching is required, do not use a buffered store, instead use a regular, fully loaded store and
    * use the {@link Ext.grid.plugin.BufferedRenderer BufferedRenderer} plugin to minimize DOM footprint.
    * @param {Function} fn The selection function to execute for each item.
    *  @param {Mixed} fn.rec The record.
    *  @param {Mixed} fn.index The index in the total dataset of the record.
    * @param {Object} [scope] The scope (`this` reference) in which the function is executed. Defaults to this PageMap.
    * @return {Object} The first record in this page map which returned true from the selection
    * function, or null if none was found.
    */
    findBy: function(fn, scope) {
        var me = this,
            result = null;

        scope = scope || me;
        me.forEach(function(rec, index) {
            if (fn.call(scope, rec, index)) {
                result = rec;
                return false;
            }
        });
        return result;
    },

    /**
    * Returns the index *in the whole dataset* of the first record in this page map which elicits a true return value from the
    * passed selection function.
    *
    * **IMPORTANT
    * This can ONLY find records which happen to be cached in the page cache. This will be parts of the dataset around the currently
    * visible zone, or recently visited zones if the pages have not yet been purged from the cache.
    * 
    * This CAN NOT find records which have not been loaded into the cache.**
    *
    * If full client side searching is required, do not use a buffered store, instead use a regular, fully loaded store and
    * use the {@link Ext.grid.plugin.BufferedRenderer BufferedRenderer} plugin to minimize DOM footprint.
    * @param {Function} fn The selection function to execute for each item.
    *  @param {Mixed} fn.rec The record.
    *  @param {Mixed} fn.index The index in the total dataset of the record.
    * @param {Object} [scope] The scope (`this` reference) in which the function is executed. Defaults to this PageMap.
    * @return {Number} The index first record in this page map which returned true from the selection
    * function, or -1 if none was found.
    */
    findIndexBy: function(fn, scope) {
        var me = this,
            result = -1;

        scope = scope || me;
        me.forEach(function(rec, index) {
            if (fn.call(scope, rec)) {
                result = index;
                return false;
            }
        });
        return result;
    },

    getPageFromRecordIndex: function() {
        return Ext.data.Store.prototype.getPageFromRecordIndex.apply(this, arguments);
    },

    addAll: function(records) {
        //<debug>
        if (this.getCount()) {
            Ext.Error.raise('Cannot addAll to a non-empty PageMap');
        }
        //</debug>
        this.addPage(1, records);
    },

    addPage: function(pageNumber, records) {
        var me = this,
            lastPage = pageNumber + Math.floor((records.length - 1) / me.pageSize),
            startIdx,
            page;

        // Account for being handed a block of records spanning several pages.
        // This can happen when loading from a MemoryProxy before a viewSize has been determined.
        for (startIdx = 0; pageNumber <= lastPage; pageNumber++, startIdx += me.pageSize) {
            page = Ext.Array.slice(records, startIdx, startIdx + me.pageSize);
            me.add(pageNumber, page);
            me.fireEvent('pageAdded', pageNumber, page);
        }
    },

    getCount: function() {
        var result = this.callParent();
        if (result) {
            result = (result - 1) * this.pageSize + this.last.value.length;
        }
        return result;
    },

    indexOf: function(record) {
        return record ? record.index : -1;
    },

    insert: function() {
        //<debug>
        Ext.Error.raise('insert operation not suppported into buffered Store');
        //</debug>
    },

    remove: function() {
        //<debug>
        Ext.Error.raise('remove operation not suppported from buffered Store');
        //</debug>
    },

    removeAt: function() {
        //<debug>
        Ext.Error.raise('removeAt operation not suppported from buffered Store');
        //</debug>
    },

    getPage: function(pageNumber) {
        return this.get(pageNumber);
    },

    hasRange: function(start, end) {
        var pageNumber = this.getPageFromRecordIndex(start),
            endPageNumber = this.getPageFromRecordIndex(end);

        for (; pageNumber <= endPageNumber; pageNumber++) {
            if (!this.hasPage(pageNumber)) {
                return false;
            }
        }
        return true;
    },

    hasPage: function(pageNumber) {
        // We must use this.get to trigger an access so that the page which is checked for presence is not eligible for pruning
        return !!this.get(pageNumber);
    },

    getAt: function(index) {
        return this.getRange(index, index)[0];
    },

    getRange: function(start, end) {
        if (!this.hasRange(start, end)) {
            Ext.Error.raise('PageMap asked for range which it does not have');
        }
        var me = this,
            startPageNumber = me.getPageFromRecordIndex(start),
            endPageNumber = me.getPageFromRecordIndex(end),
            dataStart = (startPageNumber - 1) * me.pageSize,
            dataEnd = (endPageNumber * me.pageSize) - 1,
            pageNumber = startPageNumber,
            result = [],
            sliceBegin, sliceEnd, doSlice,
            i = 0, len;

        for (; pageNumber <= endPageNumber; pageNumber++) {

            // First and last pages will need slicing to cut into the actual wanted records
            if (pageNumber == startPageNumber) {
                sliceBegin = start - dataStart;
                doSlice = true;
            } else {
                sliceBegin = 0;
                doSlice = false;
            }
            if (pageNumber == endPageNumber) {
                sliceEnd = me.pageSize - (dataEnd - end);
                doSlice = true;
            }

            // First and last pages will need slicing
            if (doSlice) {
                Ext.Array.push(result, Ext.Array.slice(me.getPage(pageNumber), sliceBegin, sliceEnd));
            } else {
                Ext.Array.push(result, me.getPage(pageNumber));
            }
        }

        // Inject the dataset ordinal position into the record as the index
        for (len = result.length; i < len; i++) {
            result[i].index = start++;
        }
        return result;
    }
});