/* *
 *
 *  Grid Rows Renderer class.
 *
 *  (c) 2020-2026 Highsoft AS
 *
 *  Integration of this software requires a license.
 *  - For commercial use, see www.highcharts.com/license
 *  - For non-commercial, see www.highcharts.com/license-eula
 *
 *
 *  Authors:
 *  - Dawid Draguła
 *
 * */
'use strict';
import TableRow from '../Body/TableRow.js';
import Globals from '../../Globals.js';
import { defined } from '../../../../Shared/Utilities.js';
/* *
 *
 *  Class
 *
 * */
/**
 * Represents a virtualized rows renderer for the data grid.
 */
class RowsVirtualizer {
    /**
     * The maximum height of a HTML element in most browsers.
     * Firefox has a lower limit than other browsers.
     */
    static getMaxElementHeight() {
        if (RowsVirtualizer.maxElementHeight !== void 0) {
            return RowsVirtualizer.maxElementHeight;
        }
        const isFirefox = Globals.userAgent.indexOf('Firefox') > -1;
        const fallbackMax = ((isFirefox ? 6000000 : 31000000) /
            (window.devicePixelRatio || 1));
        if (!document.body) {
            RowsVirtualizer.maxElementHeight = fallbackMax;
            return RowsVirtualizer.maxElementHeight;
        }
        let res = 1000000;
        const testUpTo = isFirefox ? 6000000 : 1000000000;
        const div = document.createElement('div');
        document.body.appendChild(div);
        let done = false;
        while (!done) {
            const test = res * 2;
            div.style.height = test + 'px';
            if (test > testUpTo || div.clientHeight !== test) {
                done = true;
                continue;
            }
            res = test;
        }
        div.remove();
        const safeMax = 16000000;
        RowsVirtualizer.maxElementHeight = Math.min(res || fallbackMax, safeMax);
        return RowsVirtualizer.maxElementHeight;
    }
    /* *
    *
    *  Constructor
    *
    * */
    /**
     * Constructs an instance of the rows virtualizer.
     *
     * @param viewport
     * The viewport of the data grid to render rows in.
     */
    constructor(viewport) {
        /* *
        *
        *  Properties
        *
        * */
        /**
         * The default height of a row.
         */
        this.defaultRowHeight = 49;
        /**
         * The index of the first visible row.
         */
        this.rowCursor = 0;
        /**
         * Flag indicating if the scrolling handler should be prevented to avoid
         * flickering loops when scrolling to the last row.
         */
        this.preventScroll = false;
        /**
         * The total height of the grid, used when the Grid height
         * exceeds the max element height.
         */
        this.totalGridHeight = 0;
        /**
         * The overflow height of the grid, used when the Grid height
         * exceeds the max element height.
         */
        this.gridHeightOverflow = 0;
        /**
         * The scroll offset in pixels used to adjust the row positions when
         * the Grid height exceeds the max element height.
         */
        this.scrollOffset = 0;
        /**
         * Reuse pool for rows that are currently out of viewport.
         */
        this.rowPool = [];
        /**
         * Flag indicating if a scroll update is queued for the next animation
         * frame.
         */
        this.scrollQueued = false;
        /**
         * Flag indicating if rows are currently being rendered to prevent
         * concurrent render operations.
         */
        this.isRendering = false;
        /**
         * Pending row cursor to render after current render completes.
         * Used to ensure the final scroll position is rendered.
         */
        this.pendingRowCursor = null;
        const rowSettings = (viewport.grid.options?.rendering?.rows || {});
        this.rowSettings = rowSettings;
        this.viewport = viewport;
        this.rowCount = 0;
        this.strictRowHeights = !!rowSettings.strictHeights;
        this.buffer = Math.max((rowSettings.bufferSize || 0), 0);
        this.maxElementHeight = RowsVirtualizer.getMaxElementHeight();
        this.rowCursor = 0;
        if (this.strictRowHeights) {
            viewport.tbodyElement.classList.add(Globals.getClassName('rowsContentNowrap'));
        }
    }
    /* *
    *
    *  Functions
    *
    * */
    /**
     * Renders the rows in the viewport for the first time.
     */
    async initialRender() {
        this.defaultRowHeight = await this.getDefaultRowHeight();
        // Initial reflow to set the viewport height
        if (this.viewport.virtualRows) {
            this.viewport.reflow();
        }
        await this.beforeInitialRenderRows?.();
        // Load & render rows
        await this.renderRows(this.rowCursor);
        this.adjustRowHeights();
        if (this.viewport.virtualRows) {
            this.adjustRowOffsets();
        }
    }
    /**
     * Renders the rows in the viewport. It is called when the rows need to be
     * re-rendered, e.g., after a sort or filter operation.
     */
    async rerender() {
        const tbody = this.viewport.tbodyElement;
        let rows = this.viewport.rows;
        const oldScrollLeft = tbody.scrollLeft;
        let oldScrollTop;
        if (this.rowPool.length) {
            for (let i = this.rowPool.length - 1; i >= 0; --i) {
                this.rowPool[i].destroy();
            }
            this.rowPool.length = 0;
        }
        if (rows.length) {
            oldScrollTop = tbody.scrollTop;
            for (let i = 0, iEnd = rows.length; i < iEnd; ++i) {
                rows[i].destroy();
            }
            rows.length = 0;
        }
        await this.renderRows(this.rowCursor);
        if (this.viewport.virtualRows) {
            if (defined(oldScrollTop)) {
                tbody.scrollTop = oldScrollTop;
            }
        }
        rows = this.viewport.rows;
        // Reflow the rendered row cells widths (check redundancy)
        for (let i = 0, iEnd = rows.length; i < iEnd; ++i) {
            rows[i].reflow();
        }
        tbody.scrollLeft = oldScrollLeft;
    }
    /**
     * Refreshes the rendered rows without a full teardown.
     * It updates the row range and reuses existing rows when possible.
     */
    async refreshRows() {
        await this.updateGridMetrics();
        const tbody = this.viewport.tbodyElement;
        const oldScrollLeft = tbody.scrollLeft;
        const oldScrollTop = this.viewport.virtualRows ?
            tbody.scrollTop :
            void 0;
        const maxRowCursor = Math.max(0, this.rowCount - 1);
        if (this.rowCursor > maxRowCursor) {
            this.rowCursor = maxRowCursor;
        }
        // Render missing rows, drop out-of-range ones, and ensure last row.
        await this.renderRows(this.rowCursor);
        const rows = this.viewport.rows;
        // For non-virtualized rows, re-order rows to match data order.
        if (!this.viewport.virtualRows) {
            let node = tbody.firstElementChild;
            for (let i = 0, iEnd = rows.length; i < iEnd; ++i) {
                if (node === rows[i].htmlElement) {
                    node = node.nextElementSibling;
                    continue;
                }
                // Mismatch found, so append the rest in the correct order.
                for (let j = i; j < iEnd; ++j) {
                    tbody.appendChild(rows[j].htmlElement);
                }
                break;
            }
        }
        for (let i = 0, iEnd = rows.length; i < iEnd; ++i) {
            await rows[i].update();
        }
        if (this.viewport.virtualRows && defined(oldScrollTop)) {
            tbody.scrollTop = oldScrollTop;
        }
        tbody.scrollLeft = oldScrollLeft;
    }
    /**
     * Method called on the viewport scroll event, only when the virtualization
     * is enabled.
     */
    scroll() {
        if (this.scrollQueued) {
            return;
        }
        this.scrollQueued = true;
        requestAnimationFrame(() => {
            this.scrollQueued = false;
            void this.applyScroll();
        });
    }
    /**
     * Applies the scroll logic for virtualized rows.
     */
    async applyScroll() {
        const target = this.viewport.tbodyElement;
        const { defaultRowHeight: rowHeight } = this;
        const lastScrollTop = target.scrollTop;
        const scrollDenominator = this.maxElementHeight -
            target.clientHeight;
        const scrollPercentage = scrollDenominator > 0 ?
            lastScrollTop / scrollDenominator :
            0;
        this.scrollOffset = Math.floor(scrollPercentage * this.gridHeightOverflow);
        if (this.preventScroll) {
            if (lastScrollTop <= target.scrollTop) {
                this.preventScroll = false;
            }
            this.adjustBottomRowHeights();
            return;
        }
        // Do vertical virtual scrolling
        let rowCursor = Math.floor((target.scrollTop / rowHeight) +
            (this.scrollOffset / rowHeight));
        const maxRowCursor = Math.max(0, this.rowCount - 1);
        rowCursor = Math.min(rowCursor, maxRowCursor);
        if (this.rowCursor !== rowCursor) {
            await this.renderRows(rowCursor);
        }
        this.rowCursor = rowCursor;
        this.adjustRowHeights();
        this.adjustRowOffsets();
        if (!this.strictRowHeights &&
            lastScrollTop > target.scrollTop &&
            !this.preventScroll) {
            target.scrollTop = lastScrollTop;
            this.preventScroll = true;
        }
    }
    /**
     * Adjusts the visible row heights from the bottom of the viewport.
     */
    adjustBottomRowHeights() {
        const rows = this.viewport.rows;
        const rowsLn = rows.length;
        if (rowsLn < 1) {
            return;
        }
        const lastRow = rows[rowsLn - 1];
        // Skip if row is not fully rendered or has no cells
        if (!lastRow.rendered ||
            !lastRow.cells.length ||
            !lastRow.cells[0]?.htmlElement) {
            return;
        }
        let rowTop = lastRow.translateY;
        const rowBottom = rowTop + lastRow.htmlElement.offsetHeight;
        let newHeight = lastRow.cells[0].htmlElement.offsetHeight;
        rowTop = rowBottom - newHeight;
        lastRow.htmlElement.style.height = newHeight + 'px';
        lastRow.setTranslateY(rowTop);
        for (let j = 0, jEnd = lastRow.cells.length; j < jEnd; ++j) {
            const cell = lastRow.cells[j];
            if (cell?.htmlElement) {
                cell.htmlElement.style.transform = '';
            }
        }
        for (let i = rowsLn - 2; i >= 0; i--) {
            const row = rows[i];
            // Skip if row is not fully rendered or has no cells
            if (!row.rendered ||
                !row.cells.length ||
                !row.cells[0]?.htmlElement) {
                continue;
            }
            newHeight = row.cells[0].htmlElement.offsetHeight;
            rowTop -= newHeight;
            row.htmlElement.style.height = newHeight + 'px';
            row.setTranslateY(rowTop);
            for (let j = 0, jEnd = row.cells.length; j < jEnd; ++j) {
                const cell = row.cells[j];
                if (cell?.htmlElement) {
                    cell.htmlElement.style.transform = '';
                }
            }
        }
    }
    /**
     * Renders rows in the specified range. Removes rows that are out of the
     * range except the last row.
     *
     * @param rowCursor
     * The index of the first visible row.
     */
    async renderRows(rowCursor) {
        // Prevent concurrent render operations. Queue the latest cursor and
        // render it once the current pass completes.
        if (this.isRendering) {
            this.pendingRowCursor = rowCursor;
            return;
        }
        this.isRendering = true;
        try {
            const { viewport: vp, buffer } = this;
            await this.updateGridMetrics();
            const rowCount = this.rowCount;
            if (rowCount === 0) {
                if (vp.rows.length) {
                    for (let i = 0, iEnd = vp.rows.length; i < iEnd; ++i) {
                        vp.rows[i].destroy();
                    }
                    vp.rows.length = 0;
                }
                vp.tbodyElement.innerHTML = '';
                this.rowCursor = 0;
                return;
            }
            const rowEnd = rowCount - 1;
            const isVirtualization = this.viewport.virtualRows;
            if (isVirtualization && vp.grid.popups.size) {
                for (const popup of Array.from(vp.grid.popups)) {
                    if (popup.anchorElement &&
                        !popup.anchorElement.isConnected) {
                        popup.hide();
                    }
                }
            }
            const rowsPerPage = isVirtualization ? Math.ceil((vp.grid.tableElement?.clientHeight || 0) /
                this.defaultRowHeight) : Infinity;
            let rows = vp.rows;
            if (!isVirtualization && rows.length > 50) {
                // eslint-disable-next-line no-console
                console.warn('Grid: a large dataset can cause performance issues when ' +
                    'virtualization is disabled. Consider enabling ' +
                    'virtualization in the rows settings.');
            }
            if (!rows.length) {
                const last = new TableRow(vp, rowEnd);
                await last.init();
                vp.tbodyElement.appendChild(last.htmlElement);
                await last.render();
                rows.push(last);
                if (isVirtualization) {
                    const topOffset = Math.min(last.getDefaultTopOffset(), this.maxElementHeight - last.htmlElement.offsetHeight);
                    last.setTranslateY(topOffset);
                }
            }
            // The last row is always kept rendered for bottom alignment.
            let alwaysLastRow = rows.length > 0 ? rows.pop() : void 0;
            if (alwaysLastRow && alwaysLastRow.index !== rowEnd) {
                this.poolRow(alwaysLastRow);
                alwaysLastRow = void 0;
            }
            const from = Math.max(0, Math.min(rowCursor - buffer, rowEnd - rowsPerPage + 1));
            // `to` should not include the alwaysLastRow index (`rowEnd`).
            const to = Math.min(rowCursor + rowsPerPage + buffer, rowEnd - 1);
            const tempRows = [];
            const currentFrom = rows[0]?.index;
            const currentTo = rows[rows.length - 1]?.index;
            const hasOverlap = (rows.length > 0 &&
                defined(currentFrom) &&
                defined(currentTo) &&
                !(to < currentFrom || from > currentTo));
            if (!hasOverlap) {
                // Remove rows that are out of the range except the last row.
                for (let i = 0, iEnd = rows.length; i < iEnd; ++i) {
                    const row = rows[i];
                    const rowIndex = row.index;
                    if (rowIndex < from || rowIndex > to) {
                        this.poolRow(row);
                    }
                    else {
                        tempRows.push(row);
                    }
                }
                rows = tempRows;
                vp.rows = rows;
                for (let i = from; i <= to; ++i) {
                    const firstRowIndex = rows.length > 0 ? rows[0].index : from;
                    const row = rows[i - firstRowIndex];
                    // Recreate the row when it is missing from the target
                    // range.
                    if (!row) {
                        rows.push(await this.getOrCreateRow(i));
                    }
                }
                rows.sort((a, b) => a.index - b.index);
            }
            else {
                // Remove rows outside the range from the start.
                while (rows.length && rows[0].index < from) {
                    this.poolRow(rows.shift());
                }
                // Remove rows outside the range from the end.
                while (rows.length && rows[rows.length - 1].index > to) {
                    this.poolRow(rows.pop());
                }
                if (!rows.length) {
                    for (let i = from; i <= to; ++i) {
                        rows.push(await this.getOrCreateRow(i));
                    }
                }
                else {
                    // Add rows before the current range.
                    for (let i = rows[0].index - 1; i >= from; --i) {
                        rows.unshift(await this.getOrCreateRow(i));
                    }
                    // Add rows after the current range.
                    const lastRowIndex = rows[rows.length - 1].index + 1;
                    for (let i = lastRowIndex; i <= to; ++i) {
                        rows.push(await this.getOrCreateRow(i));
                    }
                }
                vp.rows = rows;
            }
            for (let i = 0, iEnd = rows.length; i < iEnd; ++i) {
                const row = rows[i];
                if (!row.rendered) {
                    // Ensure the row is initialized before rendering.
                    if (!row.htmlElement.hasAttribute('data-row-index')) {
                        await row.init();
                    }
                    vp.tbodyElement.insertBefore(row.htmlElement, vp.tbodyElement.lastChild);
                    await row.render();
                    if (isVirtualization) {
                        const topOffset = Math.min(row.getDefaultTopOffset(), this.maxElementHeight - row.htmlElement.offsetHeight);
                        row.setTranslateY(topOffset);
                    }
                    continue;
                }
                if (!row.htmlElement.isConnected) {
                    vp.tbodyElement.insertBefore(row.htmlElement, vp.tbodyElement.lastChild);
                }
            }
            if (!alwaysLastRow && rowCount > 0) {
                alwaysLastRow = await this.getOrCreateRow(rowEnd);
            }
            if (alwaysLastRow) {
                if (!alwaysLastRow.rendered) {
                    if (!alwaysLastRow.htmlElement
                        .hasAttribute('data-row-index')) {
                        await alwaysLastRow.init();
                    }
                    vp.tbodyElement.appendChild(alwaysLastRow.htmlElement);
                    await alwaysLastRow.render();
                    if (isVirtualization) {
                        const topOffset = Math.min(alwaysLastRow.getDefaultTopOffset(), this.maxElementHeight -
                            alwaysLastRow.htmlElement.offsetHeight);
                        alwaysLastRow.setTranslateY(topOffset);
                    }
                }
                else if (!alwaysLastRow.htmlElement.isConnected) {
                    vp.tbodyElement.appendChild(alwaysLastRow.htmlElement);
                }
                rows.push(alwaysLastRow);
            }
            // Focus the cell if the focus cursor is set
            if (vp.pendingFocusCursor) {
                const [rowIndex, columnIndex] = vp.pendingFocusCursor;
                const row = rows.find((row) => row.index === rowIndex);
                const cell = row?.getCellByColumnIndex(columnIndex);
                // Keep the pending cursor until the target cell exists. The
                // column may still be virtualized when the row renders first,
                // in which case the column render pass restores focus later.
                if (row && cell) {
                    delete vp.pendingFocusCursor;
                    vp.restoreRenderedCellFocus(cell, rowIndex, columnIndex);
                    vp.ensureRowFullyVisible(row);
                }
            }
            else if (vp.focusCursor) {
                const { focusCursor } = vp;
                if (focusCursor.type !== 'header' &&
                    !focusCursor.bodySectionId) {
                    const focusedRow = rows.find((row) => row.id === focusCursor.rowId);
                    if (focusedRow) {
                        vp.restoreRenderedCellFocus(focusedRow.getCellByColumnIndex(focusCursor.columnIndex), focusedRow.index, focusCursor.columnIndex, false);
                    }
                }
            }
            // Set the focus anchor cell.
            if ((!vp.focusCursor ||
                vp.focusCursor.type === 'header' ||
                !vp.focusAnchorCell?.row.rendered) &&
                rows.length > 0) {
                const anchorRowIndex = Math.max(0, rowCursor - rows[0].index);
                const anchorRow = rows[anchorRowIndex];
                if (anchorRow?.cells[0]) {
                    vp.setFocusAnchorCell(anchorRow.cells[0]);
                }
            }
            await vp.syncAriaRowIndexes();
            await this.afterRenderRows?.();
        }
        finally {
            this.isRendering = false;
            // If there is a pending render request, process it now.
            if (this.pendingRowCursor !== null) {
                const pendingCursor = this.pendingRowCursor;
                this.pendingRowCursor = null;
                await this.renderRows(pendingCursor);
            }
        }
    }
    /**
     * Adjusts the heights of the rows based on the current scroll position.
     * It handles the possibility of the rows having different heights than
     * the default height.
     */
    adjustRowHeights() {
        if (this.strictRowHeights ||
            !this.viewport.virtualRows) {
            return;
        }
        const { rowCursor: cursor, defaultRowHeight: defaultH } = this;
        const { rows, tbodyElement } = this.viewport;
        const rowsLn = rows.length;
        if (rowsLn < 1 || !defaultH) {
            return;
        }
        let translateBuffer = rows[0].getDefaultTopOffset();
        for (let i = 0; i < rowsLn; ++i) {
            const row = rows[i];
            // Skip if row is not fully rendered or has no cells
            if (!row.rendered ||
                !row.cells.length ||
                !row.cells[0]?.htmlElement) {
                row.htmlElement.style.height = defaultH + 'px';
                continue;
            }
            // Reset row height and cell transforms
            row.htmlElement.style.height = '';
            if (row.cells[0].htmlElement.style.transform) {
                for (let j = 0, jEnd = row.cells.length; j < jEnd; ++j) {
                    const cell = row.cells[j];
                    if (cell?.htmlElement) {
                        cell.htmlElement.style.transform = '';
                    }
                }
            }
            // Rows above the first visible row
            if (row.index < cursor) {
                row.htmlElement.style.height = defaultH + 'px';
                continue;
            }
            const cellHeight = row.cells[0].htmlElement.offsetHeight;
            row.htmlElement.style.height = cellHeight + 'px';
            // Rows below the first visible row
            if (row.index > cursor) {
                continue;
            }
            // First visible row
            if (row.htmlElement.offsetHeight > defaultH) {
                const newHeight = Math.floor(cellHeight - (cellHeight - defaultH) * (tbodyElement.scrollTop / defaultH - Math.floor(cursor - this.scrollOffset / defaultH)));
                row.htmlElement.style.height = newHeight + 'px';
                for (let j = 0, jEnd = row.cells.length; j < jEnd; ++j) {
                    const cell = row.cells[j];
                    if (cell?.htmlElement) {
                        cell.htmlElement.style.transform = `translateY(${newHeight - cellHeight}px)`;
                    }
                }
            }
        }
        rows[0].setTranslateY(translateBuffer);
        for (let i = 1, iEnd = rowsLn - 1; i < iEnd; ++i) {
            translateBuffer += rows[i - 1].htmlElement.offsetHeight;
            rows[i].setTranslateY(translateBuffer);
        }
        // Set the proper offset for the last row
        const lastRow = rows[rowsLn - 1];
        const preLastRow = rows[rowsLn - 2];
        if (preLastRow && preLastRow.index === lastRow.index - 1) {
            lastRow.setTranslateY(preLastRow.htmlElement.offsetHeight + translateBuffer);
        }
    }
    /**
     * Reflow the rendered rows content dimensions.
     */
    reflowRows() {
        const rows = this.viewport.rows;
        if (rows.length < 1) {
            return;
        }
        for (let i = 0, iEnd = rows.length; i < iEnd; ++i) {
            rows[i].reflow();
        }
        this.adjustRowHeights();
        if (this.viewport.virtualRows) {
            this.adjustRowOffsets();
        }
    }
    /**
     * Estimates the current top offset of a row in the virtualized scroll
     * space, accounting for the active scroll compression offset.
     *
     * @param index
     * The row index in the projected data order.
     *
     * @returns
     * The estimated top offset in pixels.
     */
    getEstimatedRowTop(index) {
        const topOffset = index * this.defaultRowHeight;
        if (!this.viewport.virtualRows ||
            this.gridHeightOverflow <= 0) {
            return topOffset;
        }
        return Math.floor(topOffset - this.scrollOffset);
    }
    /**
     * Estimates the current bottom offset of a row in the virtualized scroll
     * space.
     *
     * @param index
     * The row index in the projected data order.
     *
     * @param rowHeight
     * The height of the row.
     *
     * @returns
     * The estimated bottom offset in pixels.
     */
    getEstimatedRowBottom(index, rowHeight = this.defaultRowHeight) {
        return this.getEstimatedRowTop(index) + rowHeight;
    }
    /**
     * Measures the height of the first rendered row's first cell.
     *
     * @returns
     * The measured row height or undefined if it cannot be measured.
     */
    measureRenderedRowHeight() {
        const rows = this.viewport.rows;
        if (!rows.length) {
            return;
        }
        for (let i = 0, iEnd = rows.length; i < iEnd; ++i) {
            const cell = rows[i].cells[0]?.htmlElement;
            if (cell) {
                const height = cell.offsetHeight;
                return height > 0 ? height : void 0;
            }
        }
    }
    /**
     * Applies a newly measured row height and refreshes related metrics.
     *
     * @param measuredHeight
     * The measured row height in pixels.
     *
     * @returns
     * Whether the default row height changed.
     */
    applyMeasuredRowHeight(measuredHeight) {
        const nextHeight = Math.max(1, Math.round(measuredHeight));
        if (!nextHeight || nextHeight === this.defaultRowHeight) {
            return false;
        }
        const prevHeight = this.defaultRowHeight;
        this.defaultRowHeight = nextHeight;
        this.totalGridHeight = this.rowCount * this.defaultRowHeight;
        this.gridHeightOverflow = Math.max(this.totalGridHeight - this.maxElementHeight, 0);
        const target = this.viewport.tbodyElement;
        if (this.viewport.virtualRows && prevHeight > 0) {
            target.scrollTop = target.scrollTop * nextHeight / prevHeight;
        }
        const scrollDenominator = this.maxElementHeight - target.clientHeight;
        const scrollPercentage = scrollDenominator > 0 ?
            target.scrollTop / scrollDenominator :
            0;
        this.scrollOffset = Math.floor(scrollPercentage * this.gridHeightOverflow);
        if (this.viewport.virtualRows) {
            this.adjustRowHeights();
            this.adjustRowOffsets();
        }
        return true;
    }
    /**
     * Gets a row from the pool or creates a new one for the given index.
     *
     * @param index
     * The row index in the data table.
     *
     * @returns
     * A TableRow instance ready for use.
     */
    async getOrCreateRow(index) {
        const vp = this.viewport;
        const isVirtualization = vp.virtualRows;
        const pooledRow = this.rowPool.pop();
        if (pooledRow) {
            await pooledRow.reuse(index);
            if (isVirtualization) {
                pooledRow.setTranslateY(pooledRow.getDefaultTopOffset());
            }
            return pooledRow;
        }
        const newRow = new TableRow(vp, index);
        newRow.rendered = false;
        await newRow.init();
        if (isVirtualization) {
            newRow.setTranslateY(newRow.getDefaultTopOffset());
        }
        return newRow;
    }
    /**
     * Adds a row to the reuse pool, or destroys it if the pool is full.
     *
     * @param row
     * The row to pool.
     */
    poolRow(row) {
        const { focusCursor } = this.viewport;
        const activeElement = document.activeElement;
        if (focusCursor &&
            focusCursor.type !== 'header' &&
            focusCursor.rowId === row.id &&
            activeElement instanceof Element &&
            row.htmlElement.contains(activeElement)) {
            this.viewport.preserveFocusDuringDetach();
        }
        row.htmlElement.remove();
        if (this.rowPool.length < RowsVirtualizer.MAX_POOL_SIZE) {
            this.rowPool.push(row);
        }
        else {
            row.destroy();
        }
    }
    /**
     * Returns the default height of a row. This method should be called only
     * once on initialization.
     *
     * @returns
     * The default height of a row.
     */
    async getDefaultRowHeight() {
        const vp = this.viewport;
        const mockRow = new TableRow(vp, 0);
        await mockRow.init();
        mockRow.htmlElement.style.position = 'absolute';
        mockRow.htmlElement.classList.add(Globals.getClassName('mockedRow'));
        vp.tbodyElement.appendChild(mockRow.htmlElement);
        await mockRow.render();
        const defaultRowHeight = mockRow.htmlElement.offsetHeight;
        mockRow.destroy();
        return defaultRowHeight;
    }
    /**
     * Updates cached row count and derived grid height metrics used for
     * overflow-aware scrolling.
     */
    async updateGridMetrics() {
        const providerRowCount = await this.viewport.grid.dataProvider
            ?.getRowCount();
        this.rowCount = defined(providerRowCount) ? providerRowCount : 0;
        this.rowCount = this.getEffectiveRowCount ?
            await this.getEffectiveRowCount(this.rowCount) :
            this.rowCount;
        this.totalGridHeight = this.rowCount * this.defaultRowHeight;
        this.gridHeightOverflow = Math.max(this.totalGridHeight - this.maxElementHeight, 0);
        this.rowCursor = Math.min(Math.max(this.rowCursor, 0), Math.max(0, this.rowCount - 1));
    }
    /**
     * Updates row translate offsets based on scroll scaling. When the grid
     * exceeds the max element height, it keeps the bottom rows aligned to the
     * maximum scrollable height.
     */
    adjustRowOffsets() {
        const { rows } = this.viewport;
        const rowsLn = rows.length;
        if (rowsLn < 2) {
            return;
        }
        const lastRow = rows[rowsLn - 1];
        const preLastRow = rows[rowsLn - 2];
        const isSecondToLastRowVisible = preLastRow &&
            preLastRow.index === lastRow.index - 1;
        let translateBuffer = rows[0].getDefaultTopOffset();
        translateBuffer = Math.floor(translateBuffer - this.scrollOffset);
        if (isSecondToLastRowVisible && this.gridHeightOverflow > 0) {
            lastRow.setTranslateY(this.maxElementHeight -
                lastRow.htmlElement.offsetHeight);
            let bottomOffset = this.maxElementHeight -
                lastRow.htmlElement.offsetHeight;
            for (let i = rowsLn - 2; i >= 0; i--) {
                bottomOffset -= rows[i].htmlElement.offsetHeight;
                rows[i].setTranslateY(bottomOffset);
            }
            return;
        }
        rows[0].setTranslateY(translateBuffer);
        for (let i = 1, iEnd = rowsLn - 1; i < iEnd; ++i) {
            translateBuffer += rows[i - 1].htmlElement.offsetHeight;
            rows[i].setTranslateY(translateBuffer);
        }
        if (this.gridHeightOverflow > 0) {
            lastRow.setTranslateY(this.maxElementHeight);
            return;
        }
        if (preLastRow && preLastRow.index === lastRow.index - 1) {
            lastRow.setTranslateY(preLastRow.htmlElement.offsetHeight + translateBuffer);
        }
    }
}
/**
 * Maximum number of rows to keep in the reuse pool.
 */
RowsVirtualizer.MAX_POOL_SIZE = 100;
/* *
 *
 *  Default Export
 *
 * */
export default RowsVirtualizer;
