/**
 * @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',
 
    config: {
        store: null,
 
        /**
         * @cfg {Number} pageSize
         * The size of pages in this map.
         */
        pageSize: 0,
 
        /**
         * @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: ''
    },
 
    // 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;
 
        // Map of internalId to recordIndex
        me.indexMap = {};
 
        me.callParent([initial]);
    },
 
    //<debug>
    updatePageSize: function(value, oldValue) {
        if (oldValue != null) {
            throw "pageMap page size may not be changed";
        }
    },
    //</debug>
 
    forEach: function(fn, scope) {
        var me = this,
            pageNumbers = Ext.Object.getKeys(me.map),
            pageCount = pageNumbers.length,
            pageSize = me.getPageSize(),
            pageNumber, page, i, j, len;
 
        for (= 0; i < pageCount; i++) {
            pageNumbers[i] = +pageNumbers[i];
        }
 
        Ext.Array.sort(pageNumbers, Ext.Array.numericSortFn);
        scope = scope || me;
 
        for (= 0; i < pageCount; i++) {
            pageNumber = pageNumbers[i];
            page = me.getPage(pageNumber);
            len = page.length;
 
            for (= 0; j < len; j++) {
                if (fn.call(scope, page[j], (pageNumber - 1) * 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;
    },
 
    find: function(property, value, start, startsWith, endsWith, ignoreCase) {
        if (Ext.isEmpty(value, false)) {
            return null;
        }
 
        /* eslint-disable-next-line vars-on-top */
        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);
    },
 
    findIndex: function(property, value, start, startsWith, endsWith, ignoreCase) {
        if (Ext.isEmpty(value, false)) {
            return null;
        }
 
        /* eslint-disable-next-line vars-on-top */
        var regex = Ext.String.createRegex(value, startsWith, endsWith, ignoreCase),
            root = this.getRootProperty();
 
        return this.findIndexBy(function(item) {
            return item && regex.test((root ? item[root] : item)[property]);
        }, null, start);
    },
 
    getPageFromRecordIndex: function(index) {
        return Math.floor(index / this.getPageSize()) + 1;
    },
 
    addAll: function(records) {
        //<debug>
        if (this.getCount()) {
            Ext.raise('Cannot addAll to a non-empty PageMap');
        }
        //</debug>
 
        this.addPage(1, records);
    },
 
    addPage: function(pageNumber, records) {
        var me = this,
            pageSize = me.getPageSize(),
            lastPage = pageNumber + Math.floor((records.length - 1) / pageSize),
            storeIndex = (pageNumber - 1) * pageSize,
            indexMap = me.indexMap,
            page, i, len, startIdx;
 
        // 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 += pageSize) {
            page = Ext.Array.slice(records, startIdx, startIdx + pageSize);
 
            // Maintain the indexMap so that we can implement indexOf(record)
            for (= 0, len = page.length; i < len; i++) {
                indexMap[page[i].internalId] = storeIndex++;
            }
 
            me.add(pageNumber, page);
            me.fireEvent('pageadd', me, pageNumber, page);
        }
    },
 
    getCount: function() {
        var result = this.callParent();
 
        if (result) {
            result = (result - 1) * this.getPageSize() + this.last.value.length;
        }
 
        return result;
    },
 
    getByInternalId: function(internalId) {
        var index = this.indexMap[internalId];
 
        if (index != null) {
            return this.getAt(index);
        }
    },
 
    indexOf: function(record) {
        var result = -1;
 
        if (record) {
            result = this.indexMap[record.internalId];
 
            if (result == null) {
                result = -1;
            }
        }
 
        return result;
    },
 
    insert: function() {
        //<debug>
        Ext.raise('insert operation not suppported into buffered Store');
        //</debug>
    },
 
    remove: function() {
        //<debug>
        Ext.raise('remove operation not suppported from buffered Store');
        //</debug>
    },
 
    removeAt: function() {
        //<debug>
        Ext.raise('removeAt operation not suppported from buffered Store');
        //</debug>
    },
 
    removeAtKey: function(page) {
        // Allow observers to veto
        var me = this,
            thePage = me.getPage(page),
            result, i, len;
 
        if (thePage) {
            if (me.fireEvent('beforepageremove', me, page, thePage) !== false) {
                len = thePage.length;
 
                for (= 0; i < len; i++) {
                    delete me.indexMap[thePage[i].internalId];
                }
 
                result = me.callParent(arguments);
                me.fireEvent('pageremove', me, page, thePage);
 
                // Empty the page array *after* informing observers that the records have exited.
                thePage.length = 0;
            }
        }
 
        return result;
    },
 
    getPage: function(pageNumber) {
        return this.get(pageNumber);
    },
 
    hasRange: function(start, end) {
        var me = this,
            pageNumber = me.getPageFromRecordIndex(start),
            endPageNumber = me.getPageFromRecordIndex(end);
 
        for (; pageNumber <= endPageNumber; pageNumber++) {
            if (!me.hasPage(pageNumber)) {
                return false;
            }
        }
 
        // Check that the last page is filled enough to encapsulate the range.
        return (endPageNumber - 1) * me._pageSize + me.getPage(endPageNumber).length > end;
    },
 
    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);
    },
 
    peekPage: function(pageNumber) {
        return this.map[pageNumber];
    },
 
    getAt: function(index) {
        return this.getRange(index, index + 1)[0];
    },
 
    getRange: function(start, end) {
        // Store's backing Collection now uses EXCLUSIVE endIndex
        // So store will always pass the endIndex+1 unless it's 0
        if (end) {
            end--;
        }
 
        if (!this.hasRange(start, end)) {
            Ext.raise('PageMap asked for range which it does not have');
        }
 
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            Array = Ext.Array,
            pageSize = me.getPageSize(),
            startPageNumber = me.getPageFromRecordIndex(start),
            endPageNumber = me.getPageFromRecordIndex(end),
            dataStart = (startPageNumber - 1) * pageSize,
            dataEnd = (endPageNumber * pageSize) - 1,
            pageNumber = startPageNumber,
            result = [],
            sliceBegin, sliceEnd, doSlice;
 
        for (; pageNumber <= endPageNumber; pageNumber++) {
 
            // First and last pages *may* need slicing to cut into the actual wanted records
            if (pageNumber === startPageNumber) {
                sliceBegin = start - dataStart;
                doSlice = sliceBegin > 0;
            }
            else {
                sliceBegin = 0;
                doSlice = false;
            }
 
            if (pageNumber === endPageNumber) {
                sliceEnd = pageSize - (dataEnd - end);
                doSlice = doSlice || sliceEnd < pageSize;
            }
 
            // First and last pages will need slicing
            if (doSlice) {
                Array.push(result, Array.slice(me.getPage(pageNumber), sliceBegin, sliceEnd));
            }
            else {
                Array.push(result, me.getPage(pageNumber));
            }
        }
 
        return result;
    }
});