import { addEngineCounter } from './perf/engine-counters.js';
export const BLOCK_ROWS = 128;
export const BLOCK_COLS = 32;
const MAX_POOLED_SHEET_GRID_BLOCKS = 96;
const pooledSheetGridBlocks = [];
function blockKey(row, col) {
    return Math.floor(row / BLOCK_ROWS) * 1_000_000 + Math.floor(col / BLOCK_COLS);
}
function blockIntersectsScope(axis, key, scope) {
    if (!scope) {
        return true;
    }
    const blockStart = axis === 'row' ? Math.floor(key / 1_000_000) * BLOCK_ROWS : (key % 1_000_000) * BLOCK_COLS;
    const blockEnd = blockStart + (axis === 'row' ? BLOCK_ROWS : BLOCK_COLS);
    if (scope.end === undefined) {
        return blockEnd > scope.start;
    }
    return blockEnd > scope.start && blockStart < scope.end;
}
function localAxisRangeForBlock(axis, key, scope) {
    const blockSize = axis === 'row' ? BLOCK_ROWS : BLOCK_COLS;
    if (!scope) {
        return { start: 0, end: blockSize };
    }
    const blockStart = axis === 'row' ? Math.floor(key / 1_000_000) * BLOCK_ROWS : (key % 1_000_000) * BLOCK_COLS;
    const localStart = Math.max(scope.start - blockStart, 0);
    const localEnd = scope.end === undefined ? blockSize : Math.min(scope.end - blockStart, blockSize);
    return {
        start: Math.min(localStart, blockSize),
        end: Math.max(Math.min(localEnd, blockSize), 0),
    };
}
function blockIsEmpty(block) {
    for (let index = 0; index < block.length; index += 1) {
        if (block[index] !== 0) {
            return false;
        }
    }
    return true;
}
export class SheetGrid {
    counters;
    logicalLookup;
    blocks = new Map();
    constructor(counters, logicalLookup) {
        this.counters = counters;
        this.logicalLookup = logicalLookup;
    }
    setInBlocks(blocks, row, col, cellIndex) {
        const key = blockKey(row, col);
        let block = blocks.get(key);
        if (!block) {
            block = takeSheetGridBlock();
            blocks.set(key, block);
        }
        const offset = (row % BLOCK_ROWS) * BLOCK_COLS + (col % BLOCK_COLS);
        block[offset] = cellIndex + 1;
    }
    get(row, col) {
        if (this.logicalLookup) {
            return this.logicalLookup.get(row, col) ?? -1;
        }
        return this.getPhysical(row, col);
    }
    getPhysical(row, col) {
        const block = this.blocks.get(blockKey(row, col));
        if (!block)
            return -1;
        const offset = (row % BLOCK_ROWS) * BLOCK_COLS + (col % BLOCK_COLS);
        const value = block[offset];
        return value === 0 ? -1 : value - 1;
    }
    createPhysicalGetter() {
        let cachedKey = -1;
        let cachedBlock;
        return (row, col) => {
            const key = blockKey(row, col);
            const block = key === cachedKey ? cachedBlock : this.blocks.get(key);
            cachedKey = key;
            cachedBlock = block;
            if (!block)
                return -1;
            const offset = (row % BLOCK_ROWS) * BLOCK_COLS + (col % BLOCK_COLS);
            const value = block[offset];
            return value === 0 ? -1 : value - 1;
        };
    }
    forEachPhysicalRangeEntry(rowStart, colStart, rowEnd, colEnd, fn) {
        const blockRowStart = Math.floor(rowStart / BLOCK_ROWS);
        const blockRowEnd = Math.floor(rowEnd / BLOCK_ROWS);
        const blockColStart = Math.floor(colStart / BLOCK_COLS);
        const blockColEnd = Math.floor(colEnd / BLOCK_COLS);
        for (let blockRow = blockRowStart; blockRow <= blockRowEnd; blockRow += 1) {
            const absoluteBlockRow = blockRow * BLOCK_ROWS;
            const localRowStart = Math.max(rowStart - absoluteBlockRow, 0);
            const localRowEnd = Math.min(rowEnd - absoluteBlockRow, BLOCK_ROWS - 1);
            for (let blockCol = blockColStart; blockCol <= blockColEnd; blockCol += 1) {
                const block = this.blocks.get(blockRow * 1_000_000 + blockCol);
                if (!block) {
                    continue;
                }
                const absoluteBlockCol = blockCol * BLOCK_COLS;
                const localColStart = Math.max(colStart - absoluteBlockCol, 0);
                const localColEnd = Math.min(colEnd - absoluteBlockCol, BLOCK_COLS - 1);
                for (let localRow = localRowStart; localRow <= localRowEnd; localRow += 1) {
                    const row = absoluteBlockRow + localRow;
                    const rowOffset = localRow * BLOCK_COLS;
                    for (let localCol = localColStart; localCol <= localColEnd; localCol += 1) {
                        const value = block[rowOffset + localCol];
                        if (value === 0) {
                            continue;
                        }
                        fn(value - 1, row, absoluteBlockCol + localCol);
                    }
                }
            }
        }
    }
    forEachPhysicalColumnEntry(col, fn) {
        const blockCol = Math.floor(col / BLOCK_COLS);
        const localCol = col % BLOCK_COLS;
        for (const [key, block] of this.blocks) {
            if (key % 1_000_000 !== blockCol) {
                continue;
            }
            const blockRow = Math.floor(key / 1_000_000);
            const absoluteBlockRow = blockRow * BLOCK_ROWS;
            for (let localRow = 0; localRow < BLOCK_ROWS; localRow += 1) {
                const value = block[localRow * BLOCK_COLS + localCol];
                if (value !== 0) {
                    fn(value - 1, absoluteBlockRow + localRow);
                }
            }
        }
    }
    set(row, col, cellIndex) {
        this.setInBlocks(this.blocks, row, col, cellIndex);
    }
    createRowMajorSetter() {
        let cachedKey = -1;
        let cachedBlock;
        return (row, col, cellIndex) => {
            const key = blockKey(row, col);
            let block = key === cachedKey ? cachedBlock : this.blocks.get(key);
            if (!block) {
                block = takeSheetGridBlock();
                this.blocks.set(key, block);
            }
            cachedKey = key;
            cachedBlock = block;
            const offset = (row % BLOCK_ROWS) * BLOCK_COLS + (col % BLOCK_COLS);
            block[offset] = cellIndex + 1;
        };
    }
    setDenseRowMajor(rowStart, colStart, rowCount, colCount, firstCellIndex) {
        this.setDenseRowMajorBlocks(rowStart, colStart, rowCount, colCount, firstCellIndex);
    }
    setDenseRowMajorBlocks(rowStart, colStart, rowCount, colCount, firstCellIndex) {
        const colEnd = colStart + colCount;
        if (colCount > 0 && colCount <= BLOCK_COLS && Math.floor(colStart / BLOCK_COLS) === Math.floor((colEnd - 1) / BLOCK_COLS)) {
            this.setDenseRowMajorSingleBlockColumn(rowStart, colStart, rowCount, colCount, firstCellIndex);
            return;
        }
        for (let rowOffset = 0; rowOffset < rowCount; rowOffset += 1) {
            const row = rowStart + rowOffset;
            const blockRow = Math.floor(row / BLOCK_ROWS);
            const blockRowBase = blockRow * BLOCK_ROWS;
            const rowBlockKeyBase = blockRow * 1_000_000;
            const rowBlockOffset = (row - blockRowBase) * BLOCK_COLS;
            const rowBaseCellIndex = firstCellIndex + rowOffset * colCount;
            let col = colStart;
            while (col < colEnd) {
                const blockCol = Math.floor(col / BLOCK_COLS);
                const blockColBase = blockCol * BLOCK_COLS;
                const localColStart = col - blockColBase;
                const localColEnd = Math.min(BLOCK_COLS, colEnd - blockColBase);
                const key = rowBlockKeyBase + blockCol;
                let block = this.blocks.get(key);
                if (!block) {
                    block = takeSheetGridBlock();
                    this.blocks.set(key, block);
                }
                for (let localCol = localColStart; localCol < localColEnd; localCol += 1) {
                    block[rowBlockOffset + localCol] = rowBaseCellIndex + blockColBase + localCol - colStart + 1;
                }
                col = blockColBase + localColEnd;
            }
        }
    }
    setDenseRowMajorSingleBlockColumn(rowStart, colStart, rowCount, colCount, firstCellIndex) {
        const rowEnd = rowStart + rowCount;
        const blockCol = Math.floor(colStart / BLOCK_COLS);
        const blockColBase = blockCol * BLOCK_COLS;
        const localColStart = colStart - blockColBase;
        let rowOffset = 0;
        while (rowOffset < rowCount) {
            const row = rowStart + rowOffset;
            const blockRow = Math.floor(row / BLOCK_ROWS);
            const blockRowBase = blockRow * BLOCK_ROWS;
            const rowsInBlock = Math.min(rowEnd, blockRowBase + BLOCK_ROWS) - row;
            const key = blockRow * 1_000_000 + blockCol;
            let block = this.blocks.get(key);
            if (!block) {
                block = takeSheetGridBlock();
                this.blocks.set(key, block);
            }
            for (let localRowOffset = 0; localRowOffset < rowsInBlock; localRowOffset += 1) {
                const localRow = row + localRowOffset - blockRowBase;
                const rowBlockOffset = localRow * BLOCK_COLS;
                const rowBaseCellIndex = firstCellIndex + (rowOffset + localRowOffset) * colCount;
                for (let colOffset = 0; colOffset < colCount; colOffset += 1) {
                    block[rowBlockOffset + localColStart + colOffset] = rowBaseCellIndex + colOffset + 1;
                }
            }
            rowOffset += rowsInBlock;
        }
    }
    clear(row, col) {
        const block = this.blocks.get(blockKey(row, col));
        if (!block)
            return;
        const offset = (row % BLOCK_ROWS) * BLOCK_COLS + (col % BLOCK_COLS);
        block[offset] = 0;
    }
    releaseBlocksToPool() {
        for (const block of this.blocks.values()) {
            releaseSheetGridBlock(block);
        }
        this.blocks.clear();
    }
    forEachInRange(rowStart, colStart, rowEnd, colEnd, fn) {
        if (this.logicalLookup) {
            for (let row = rowStart; row <= rowEnd; row += 1) {
                for (let col = colStart; col <= colEnd; col += 1) {
                    const value = this.logicalLookup.get(row, col);
                    if (value !== undefined) {
                        fn(value);
                    }
                }
            }
            return;
        }
        for (let row = rowStart; row <= rowEnd; row += 1) {
            for (let col = colStart; col <= colEnd; col += 1) {
                const value = this.get(row, col);
                if (value !== -1) {
                    fn(value);
                }
            }
        }
    }
    forEachCell(fn) {
        this.forEachCellEntry((cellIndex) => {
            fn(cellIndex);
        });
    }
    forEachCellEntry(fn) {
        if (this.logicalLookup) {
            this.logicalLookup.forEachCellEntry(fn);
            return;
        }
        this.blocks.forEach((block, key) => {
            const blockRow = Math.floor(key / 1_000_000);
            const blockCol = key % 1_000_000;
            for (let offset = 0; offset < block.length; offset += 1) {
                const value = block[offset];
                if (value === 0)
                    continue;
                const localRow = Math.floor(offset / BLOCK_COLS);
                const localCol = offset % BLOCK_COLS;
                const row = blockRow * BLOCK_ROWS + localRow;
                const col = blockCol * BLOCK_COLS + localCol;
                if (row >= 0 && col >= 0) {
                    fn(value - 1, row, col);
                }
            }
        });
    }
    collectAxisRemapEntries(axis, remapIndex, scope) {
        const changedEntries = [];
        [...this.blocks.keys()]
            .filter((key) => blockIntersectsScope(axis, key, scope))
            .forEach((key) => {
            if (this.counters) {
                addEngineCounter(this.counters, 'sheetGridBlockScans');
            }
            const block = this.blocks.get(key);
            if (!block) {
                return;
            }
            const blockRow = Math.floor(key / 1_000_000);
            const blockCol = key % 1_000_000;
            const localAxisRange = localAxisRangeForBlock(axis, key, scope);
            if (localAxisRange.start >= localAxisRange.end) {
                return;
            }
            if (axis === 'row') {
                for (let localRow = localAxisRange.start; localRow < localAxisRange.end; localRow += 1) {
                    const row = blockRow * BLOCK_ROWS + localRow;
                    const nextRow = remapIndex(row);
                    if (nextRow === row) {
                        continue;
                    }
                    const rowOffset = localRow * BLOCK_COLS;
                    for (let localCol = 0; localCol < BLOCK_COLS; localCol += 1) {
                        const offset = rowOffset + localCol;
                        const value = block[offset];
                        if (value === 0) {
                            continue;
                        }
                        const col = blockCol * BLOCK_COLS + localCol;
                        changedEntries.push({
                            cellIndex: value - 1,
                            row,
                            col,
                            nextRow,
                            nextCol: col,
                        });
                    }
                }
                return;
            }
            for (let localCol = localAxisRange.start; localCol < localAxisRange.end; localCol += 1) {
                const col = blockCol * BLOCK_COLS + localCol;
                const nextCol = remapIndex(col);
                if (nextCol === col) {
                    continue;
                }
                for (let localRow = 0; localRow < BLOCK_ROWS; localRow += 1) {
                    const offset = localRow * BLOCK_COLS + localCol;
                    const value = block[offset];
                    if (value === 0) {
                        continue;
                    }
                    const row = blockRow * BLOCK_ROWS + localRow;
                    changedEntries.push({
                        cellIndex: value - 1,
                        row,
                        col,
                        nextRow: row,
                        nextCol,
                    });
                }
            }
        });
        return changedEntries;
    }
    someCellInAxisScope(axis, scope, predicate) {
        if (this.logicalLookup) {
            const logicalFound = this.logicalLookup.someCellInAxisScope?.(axis, scope, predicate);
            if (logicalFound !== undefined) {
                return logicalFound;
            }
            let found = false;
            this.logicalLookup.forEachCellEntry((cellIndex, row, col) => {
                if (found) {
                    return;
                }
                const axisIndex = axis === 'row' ? row : col;
                const inScope = scope.end === undefined ? axisIndex >= scope.start : axisIndex >= scope.start && axisIndex < scope.end;
                if (inScope && predicate(cellIndex, row, col)) {
                    found = true;
                }
            });
            return found;
        }
        for (const key of this.blocks.keys()) {
            if (!blockIntersectsScope(axis, key, scope)) {
                continue;
            }
            const block = this.blocks.get(key);
            if (!block) {
                continue;
            }
            const blockRow = Math.floor(key / 1_000_000);
            const blockCol = key % 1_000_000;
            const localAxisRange = localAxisRangeForBlock(axis, key, scope);
            if (localAxisRange.start >= localAxisRange.end) {
                continue;
            }
            if (axis === 'row') {
                for (let localRow = localAxisRange.start; localRow < localAxisRange.end; localRow += 1) {
                    const row = blockRow * BLOCK_ROWS + localRow;
                    const rowOffset = localRow * BLOCK_COLS;
                    for (let localCol = 0; localCol < BLOCK_COLS; localCol += 1) {
                        const value = block[rowOffset + localCol];
                        if (value === 0) {
                            continue;
                        }
                        const col = blockCol * BLOCK_COLS + localCol;
                        if (predicate(value - 1, row, col)) {
                            return true;
                        }
                    }
                }
                continue;
            }
            for (let localCol = localAxisRange.start; localCol < localAxisRange.end; localCol += 1) {
                const col = blockCol * BLOCK_COLS + localCol;
                for (let localRow = 0; localRow < BLOCK_ROWS; localRow += 1) {
                    const value = block[localRow * BLOCK_COLS + localCol];
                    if (value === 0) {
                        continue;
                    }
                    const row = blockRow * BLOCK_ROWS + localRow;
                    if (predicate(value - 1, row, col)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
    remapAxis(axis, remapIndex, scope) {
        const changedEntries = this.collectAxisRemapEntries(axis, remapIndex, scope);
        const touchedBlockKeys = new Set();
        changedEntries.forEach(({ row, col, nextRow, nextCol }) => {
            touchedBlockKeys.add(blockKey(row, col));
            if (nextRow !== undefined && nextCol !== undefined) {
                touchedBlockKeys.add(blockKey(nextRow, nextCol));
            }
        });
        changedEntries.forEach(({ row, col }) => {
            this.clear(row, col);
        });
        changedEntries.forEach(({ cellIndex, nextRow, nextCol }) => {
            if (nextRow === undefined || nextCol === undefined) {
                return;
            }
            this.set(nextRow, nextCol, cellIndex);
        });
        touchedBlockKeys.forEach((key) => {
            const block = this.blocks.get(key);
            if (block && blockIsEmpty(block)) {
                this.blocks.delete(key);
            }
        });
        return changedEntries;
    }
}
function takeSheetGridBlock() {
    return pooledSheetGridBlocks.pop() ?? new Uint32Array(BLOCK_ROWS * BLOCK_COLS);
}
function releaseSheetGridBlock(block) {
    block.fill(0);
    if (pooledSheetGridBlocks.length < MAX_POOLED_SHEET_GRID_BLOCKS) {
        pooledSheetGridBlocks.push(block);
    }
}
//# sourceMappingURL=sheet-grid.js.map