/**
 * This class manages a sparse collection of `Page` objects keyed by their page number.
 * Pages are lazily created on request by the `getPage` method.
 *
 * When pages are locked, they are scheduled to be loaded. The loading is prioritized by
 * the type of lock held on the page. Pages with "active" locks are loaded first while
 * those with "prefetch" locks are loaded only when no "active" locked pages are in the
 * queue.
 *
 * The value of the `concurrentLoading` config controls the maximum number of simultaneously
 * pending, page load requests.
 *
 * @private
 * @since 6.5.0
 */
Ext.define('Ext.data.virtual.PageMap', {
    requires: [
        'Ext.data.virtual.Page'
    ],

    isVirtualPageMap: true,

    config: {
        /**
         * @cfg {Number} cacheSize
         * The number of pages to retain in the `cache`.
         */
        cacheSize: 10,

        /**
         * @cfg {Number} concurrentLoading
         * The maximum number of simultaneous load requests that should be made to the
         * server for pages.
         */
        concurrentLoading: 1,

        /**
         * The number of pages in the data set.
         */
        pageCount: null
    },

    generation: 0,

    store: null,

    constructor: function(config) {
        var me = this;

        me.prefetchSortFn = me.prefetchSortFn.bind(me);

        me.initConfig(config);

        me.clear();
    },

    destroy: function() {
        this.clear(true);
        this.callParent();
    },

    canSatisfy: function(range) {
        var end = this.getPageIndex(range.end),
            pageCount = this.getPageCount();

        return pageCount === null || end < pageCount;
    },

    clear: function(destroy) {
        var me = this,
            alive = !destroy || null,
            pages = me.pages,
            pg;

        ++me.generation;

        /**
         * @property {Object} byId
         * A map of records by their `idProperty`.
         */
        me.byId = alive && {};

        /**
         * @property {Object} byInternalId
         * A map of records by their `internalId`.
         */
        me.byInternalId = alive && {};

        /**
         * @property {Ext.data.virtual.Page[]} cache
         * The array of unlocked pages with the oldest at the front and the newest (most
         * recently unlocked) page at the end.
         * @readonly
         */
        me.cache = alive && [];

        /**
         * @property {Object} indexMap
         * A map of record indices by their `internalId`.
         */
        me.indexMap = alive && {};

        /**
         * @property {Object} pages
         * The sparse collection of `Page` objects keyed by their page number.
         * @readonly
         */
        me.pages = alive && {};

        /**
         * @property {Ext.data.virtual.Page[]} loading
         * The array of currently loading pages.
         */
        me.loading = alive && [];

        /**
         * @property {Object} loadQueues
         * A collection of loading queues keyed by the lock state.
         * @property {Ext.data.virtual.Page[]} loadQueues.active The queue of pages to
         * load that have an "active" lock state.
         * @property {Ext.data.virtual.Page[]} loadQueues.prefetch The queue of pages to
         * load that have a "prefetch" lock state.
         */
        me.loadQueues = alive && {
            active: [],
            prefetch: []
        };

        if (pages) {
            for (pg in pages) {
                me.destroyPage(pages[pg]);
            }
        }
    },

    getPage: function(number, autoCreate) {
        var me = this,
            pageCount = me.getPageCount(),
            pages = me.pages,
            page;

        if (pageCount === null || number < pageCount) {
            page = pages[number];

            if (!page && autoCreate !== false) {
                pages[number] = page = new Ext.data.virtual.Page({
                    pageMap: me,
                    number: number
                });
            }
        }
        //<debug>
        else {
            Ext.raise('Invalid page number ' + number + ' when limit is ' + pageCount);
        }
        //</debug>

        return page || null;
    },

    getPageIndex: function(index) {
        if (index.isEntity) {
            index = this.indexOf(index);
        }

        return Math.floor(index / this.store.getPageSize());
    },

    getPageOf: function(index, autoCreate) {
        var pageSize = this.store.getPageSize(),
            n = Math.floor(index / pageSize);

        return this.getPage(n, autoCreate);
    },

    getPages: function(begin, end) {
        var pageSize = this.store.getPageSize(),
            // Convert record indices into page numbers:
            first = Math.floor(begin / pageSize),
            last = Math.ceil(end / pageSize),
            ret = {},
            n;

        for (n = first; n < last; ++n) {
            ret[n] = this.getPage(n);
        }

        return ret;
    },

    flushNextLoad: function() {
        var me = this,
            queueTimer = me.queueTimer;

        if (queueTimer) {
            Ext.unasap(queueTimer);
        }

        me.loadNext();
    },

    indexOf: function(record) {
        var ret;

        // return indexMap if record is not null/undefined
        if (record) {
            ret = this.indexMap[record.internalId];
        }

        return (ret || ret === 0) ? ret : -1;
    },

    getByInternalId: function(internalId) {
        var index = this.indexMap[internalId],
            page;

        if (index || index === 0) {
            page = this.pages[Math.floor(index / this.store.getPageSize())];

            if (page) {
                return page.records[index - page.begin];
            }
        }
    },

    updatePageCount: function(pageCount, oldPageCount) {
        var pages = this.pages,
            pageNumber, page;

        if (oldPageCount === null || pageCount < oldPageCount) {
            // Safe to delete during a for in
            for (pageNumber in pages) {
                page = pages[pageNumber];

                if (page.number >= pageCount) {
                    this.clearPage(page);
                    this.destroyPage(page);
                }
            }
        }
    },

    privates: {
        queueTimer: null,

        clearPage: function(page, fromCache) {
            var me = this,
                A = Ext.Array,
                loadQueues = me.loadQueues;

            delete me.pages[page.number];
            page.clearRecords(me.byId, 'id');
            page.clearRecords(me.byInternalId, 'internalId');
            page.clearRecords(me.indexMap, 'internalId');

            A.remove(loadQueues.active, page);
            A.remove(loadQueues.prefetch, page);

            if (!fromCache) {
                Ext.Array.remove(me.cache, page);
            }
        },

        destroyPage: function(page) {
            this.store.onPageDestroy(page);
            page.destroy();
        },

        loadNext: function() {
            var me = this,
                loading = me.loading,
                loadQueues = me.loadQueues,
                concurrency, page;

            if (me.destroyed) {
                return;
            }

            concurrency = me.getConcurrentLoading();
            me.queueTimer = null;

            // Keep pulling from the queue(s) as long as we have more concurrency
            // allowed...
            while (loading.length < concurrency) {
                if (!(page = loadQueues.active.shift() || loadQueues.prefetch.shift())) {
                    break;
                }

                loading.push(page);
                page.load();
            }
        },

        onPageLoad: function(page) {
            var me = this,
                store = me.store,
                activeRanges = store.activeRanges,
                n = activeRanges.length,
                i;

            Ext.Array.remove(me.loading, page);

            if (!page.error) {
                page.fillRecords(me.byId, 'id');
                page.fillRecords(me.byInternalId, 'internalId');
                page.fillRecords(me.indexMap, 'internalId', true);

                store.onPageDataAcquired(page);

                for (i = 0; i < n; ++i) {
                    activeRanges[i].onPageLoad(page);
                }
            }

            me.flushNextLoad();
        },

        onPageLockChange: function(page, state, oldState) {
            var me = this,
                cache = me.cache,
                loadQueues = me.loadQueues,
                store = me.store,
                cacheSize, concurrency;

            // When a page that has never been loaded becomes locked, we want to put
            // it in the appropriate loadQueue. It is also possible for the lock state
            // to change while waiting in a loadQueue, so we may need to move it around
            // while it waits...
            if (page.isInitial()) {
                if (oldState) {
                    Ext.Array.remove(loadQueues[oldState], page);
                }

                if (state) {
                    loadQueues[state].push(page);
                    concurrency = me.getConcurrentLoading();

                    // Initiating loads immediately can easily cause problems, so wait
                    // for a tick before firing off the loads.
                    if (!me.queueTimer && me.loading.length < concurrency) {
                        me.queueTimer = Ext.asap(me.loadNext, me);
                    }
                }
            }

            if (state) {
                if (!oldState) {
                    // Make sure the page is not in the LRU queue for recycling. If it
                    // was previously not locked (!oldState) then the page is in line
                    // for removal...
                    Ext.Array.remove(cache, page);
                }
            }
            else {
                cache.push(page); // put MRU item at the end

                for (cacheSize = me.getCacheSize(); cache.length > cacheSize;) {
                    page = cache.shift();
                    me.clearPage(page, true); // remove LRU item
                    store.onPageEvicted(page);
                    me.destroyPage(page);
                }
            }
        },

        prefetchSortFn: function(a, b) {
            a = a.number;
            b = b.number;

            /* eslint-disable-next-line vars-on-top */
            var M = Math,
                firstPage = this.sortFirstPage,
                lastPage = this.sortLastPage,
                direction = this.sortDirection,
                aDir = a < firstPage,
                bDir = b < firstPage,
                ret;

            a = aDir ? M.abs(firstPage - a) : M.abs(lastPage - a);
            b = bDir ? M.abs(firstPage - b) : M.abs(lastPage - b);

            if (a === b) {
                ret = aDir ? direction : -direction;
            }
            else {
                ret = a - b;
            }

            return ret;
        },

        prioritizePrefetch: function(direction, firstPage, lastPage) {
            var me = this;

            me.sortDirection = direction;
            me.sortFirstPage = firstPage;
            me.sortLastPage = lastPage;

            me.loadQueues.prefetch.sort(me.prefetchSortFn);
        }
    }
});