import { ValueTag } from '@bilig/protocol';
import { BLOCK_COLS, BLOCK_ROWS } from '../../sheet-grid.js';
import { addEngineCounter } from '../../perf/engine-counters.js';
function getColumnSliceCacheKey(sheetName, col, rowStart, rowEnd) {
    return `${sheetName}\t${col}\t${rowStart}\t${rowEnd}`;
}
function getColumnOwnerCacheKey(sheetName, col) {
    return `${sheetName}\t${col}`;
}
function decodeValueTag(rawTag) {
    if (rawTag === undefined) {
        return ValueTag.Empty;
    }
    switch (rawTag) {
        case 1:
            return ValueTag.Number;
        case 2:
            return ValueTag.Boolean;
        case 3:
            return ValueTag.String;
        case 4:
            return ValueTag.Error;
        case 0:
        default:
            return ValueTag.Empty;
    }
}
export function createEngineRuntimeColumnStoreService(args) {
    const emptyColumnVersions = new Uint32Array(0);
    const normalizedStrings = new Map();
    const columnOwnerCache = new Map();
    const columnSliceCache = new Map();
    const normalizeStringId = (stringId) => {
        let normalized = normalizedStrings.get(stringId);
        if (normalized === undefined) {
            normalized = args.state.strings.get(stringId).toUpperCase();
            normalizedStrings.set(stringId, normalized);
        }
        return normalized;
    };
    const materializeCellValueFromRaw = (rawTag, numeric, stringId, errorCode) => {
        const tag = decodeValueTag(rawTag);
        switch (tag) {
            case ValueTag.Empty:
                return { tag: ValueTag.Empty };
            case ValueTag.Number:
                return { tag: ValueTag.Number, value: numeric };
            case ValueTag.Boolean:
                return { tag: ValueTag.Boolean, value: numeric !== 0 };
            case ValueTag.String:
                return {
                    tag: ValueTag.String,
                    value: stringId === 0 ? '' : args.state.strings.get(stringId),
                    stringId,
                };
            case ValueTag.Error:
                return { tag: ValueTag.Error, code: errorCode };
            default:
                return { tag: ValueTag.Empty };
        }
    };
    const readOwnerPageEntry = (owner, absoluteRow) => {
        const pageRowStart = Math.floor(absoluteRow / BLOCK_ROWS) * BLOCK_ROWS;
        const page = owner.pages.get(pageRowStart);
        if (!page) {
            return {
                rawTag: ValueTag.Empty,
                number: 0,
                stringId: 0,
                error: 0,
            };
        }
        const localRow = absoluteRow - pageRowStart;
        return {
            rawTag: page.tags[localRow] ?? ValueTag.Empty,
            number: page.numbers[localRow] ?? 0,
            stringId: page.stringIds[localRow] ?? 0,
            error: page.errors[localRow] ?? 0,
        };
    };
    const materializeCellValueFromOwner = (owner, absoluteRow) => {
        const entry = readOwnerPageEntry(owner, absoluteRow);
        return materializeCellValueFromRaw(entry.rawTag, entry.number, entry.stringId, entry.error);
    };
    const materializeCellValueFromPage = (page, localRow) => {
        if (!page) {
            return { tag: ValueTag.Empty };
        }
        return materializeCellValueFromRaw(page.tags[localRow], page.numbers[localRow] ?? 0, page.stringIds[localRow] ?? 0, page.errors[localRow] ?? 0);
    };
    const materializeCellValueFromCellIndex = (cellIndex) => {
        switch (args.state.workbook.cellStore.tags[cellIndex] ?? 0) {
            case 1:
                return { tag: ValueTag.Number, value: args.state.workbook.cellStore.numbers[cellIndex] ?? 0 };
            case 2:
                return { tag: ValueTag.Boolean, value: (args.state.workbook.cellStore.numbers[cellIndex] ?? 0) !== 0 };
            case 3: {
                const stringId = args.state.workbook.cellStore.stringIds[cellIndex] ?? 0;
                return {
                    tag: ValueTag.String,
                    value: stringId === 0 ? '' : args.state.strings.get(stringId),
                    stringId,
                };
            }
            case 4:
                return { tag: ValueTag.Error, code: args.state.workbook.cellStore.errors[cellIndex] };
            case 0:
            default:
                return { tag: ValueTag.Empty };
        }
    };
    const buildColumnOwner = (request) => {
        if (args.state.counters) {
            addEngineCounter(args.state.counters, 'columnOwnerBuilds');
        }
        const sheet = args.state.workbook.getSheet(request.sheetName);
        const sheetColumnVersions = sheet?.columnVersions ?? emptyColumnVersions;
        const structureVersion = sheet?.structureVersion ?? 0;
        const pages = new Map();
        if (sheet?.structureVersion === 1) {
            sheet.grid.forEachPhysicalColumnEntry(request.col, (cellIndex, row) => {
                const pageRowStart = Math.floor(row / BLOCK_ROWS) * BLOCK_ROWS;
                let page = pages.get(pageRowStart);
                if (!page) {
                    page = {
                        rowStart: pageRowStart,
                        nonEmptyCount: 0,
                        tags: new Uint8Array(BLOCK_ROWS),
                        numbers: new Float64Array(BLOCK_ROWS),
                        stringIds: new Uint32Array(BLOCK_ROWS),
                        errors: new Uint16Array(BLOCK_ROWS),
                    };
                    pages.set(pageRowStart, page);
                }
                const localRow = row - pageRowStart;
                const tag = decodeValueTag(args.state.workbook.cellStore.tags[cellIndex]);
                page.tags[localRow] = tag;
                if (tag !== ValueTag.Empty) {
                    page.nonEmptyCount += 1;
                }
                if (tag === ValueTag.Number || tag === ValueTag.Boolean) {
                    const numeric = args.state.workbook.cellStore.numbers[cellIndex] ?? 0;
                    page.numbers[localRow] = Object.is(numeric, -0) ? 0 : numeric;
                    return;
                }
                if (tag === ValueTag.String) {
                    page.stringIds[localRow] = args.state.workbook.cellStore.stringIds[cellIndex] ?? 0;
                    return;
                }
                if (tag === ValueTag.Error) {
                    page.errors[localRow] = args.state.workbook.cellStore.errors[cellIndex] ?? 0;
                }
            });
        }
        else if (sheet) {
            sheet.logical.forEachVisibleColumnCellEntry(request.col, (cellIndex, row) => {
                const pageRowStart = Math.floor(row / BLOCK_ROWS) * BLOCK_ROWS;
                let page = pages.get(pageRowStart);
                if (!page) {
                    page = {
                        rowStart: pageRowStart,
                        nonEmptyCount: 0,
                        tags: new Uint8Array(BLOCK_ROWS),
                        numbers: new Float64Array(BLOCK_ROWS),
                        stringIds: new Uint32Array(BLOCK_ROWS),
                        errors: new Uint16Array(BLOCK_ROWS),
                    };
                    pages.set(pageRowStart, page);
                }
                const localRow = row - pageRowStart;
                const tag = decodeValueTag(args.state.workbook.cellStore.tags[cellIndex]);
                page.tags[localRow] = tag;
                if (tag !== ValueTag.Empty) {
                    page.nonEmptyCount += 1;
                }
                if (tag === ValueTag.Number || tag === ValueTag.Boolean) {
                    const numeric = args.state.workbook.cellStore.numbers[cellIndex] ?? 0;
                    page.numbers[localRow] = Object.is(numeric, -0) ? 0 : numeric;
                    return;
                }
                if (tag === ValueTag.String) {
                    page.stringIds[localRow] = args.state.workbook.cellStore.stringIds[cellIndex] ?? 0;
                    return;
                }
                if (tag === ValueTag.Error) {
                    page.errors[localRow] = args.state.workbook.cellStore.errors[cellIndex] ?? 0;
                }
            });
        }
        return {
            sheetName: request.sheetName,
            col: request.col,
            columnVersion: sheetColumnVersions[request.col] ?? 0,
            structureVersion,
            sheetColumnVersions,
            pages,
        };
    };
    const getColumnOwner = (request) => {
        const cacheKey = getColumnOwnerCacheKey(request.sheetName, request.col);
        const currentSheet = args.state.workbook.getSheet(request.sheetName);
        const currentSheetColumnVersions = currentSheet?.columnVersions ?? emptyColumnVersions;
        const currentColumnVersion = currentSheetColumnVersions[request.col] ?? 0;
        const currentStructureVersion = currentSheet?.structureVersion ?? 0;
        let owner = columnOwnerCache.get(cacheKey);
        if (!owner ||
            owner.columnVersion !== currentColumnVersion ||
            owner.structureVersion !== currentStructureVersion ||
            owner.sheetColumnVersions !== currentSheetColumnVersions) {
            owner = buildColumnOwner(request);
            columnOwnerCache.set(cacheKey, owner);
        }
        return owner;
    };
    const getColumnView = (request) => {
        const owner = getColumnOwner(request);
        return {
            owner,
            sheetName: request.sheetName,
            rowStart: request.rowStart,
            rowEnd: request.rowEnd,
            col: request.col,
            length: request.rowEnd - request.rowStart + 1,
            columnVersion: owner.columnVersion,
            structureVersion: owner.structureVersion,
            sheetColumnVersions: owner.sheetColumnVersions,
            readTagAt(offset) {
                return readOwnerPageEntry(owner, request.rowStart + offset).rawTag;
            },
            readNumberAt(offset) {
                return readOwnerPageEntry(owner, request.rowStart + offset).number;
            },
            readStringIdAt(offset) {
                return readOwnerPageEntry(owner, request.rowStart + offset).stringId;
            },
            readErrorAt(offset) {
                return readOwnerPageEntry(owner, request.rowStart + offset).error;
            },
            readCellValueAt(offset) {
                return materializeCellValueFromOwner(owner, request.rowStart + offset);
            },
        };
    };
    const buildColumnSlice = (request) => {
        if (args.state.counters) {
            addEngineCounter(args.state.counters, 'columnSliceBuilds');
        }
        const view = getColumnView(request);
        const tags = new Uint8Array(view.length);
        const numbers = new Float64Array(view.length);
        const stringIds = new Uint32Array(view.length);
        const errors = new Uint16Array(view.length);
        for (let offset = 0; offset < view.length; offset += 1) {
            tags[offset] = view.readTagAt(offset);
            numbers[offset] = view.readNumberAt(offset);
            stringIds[offset] = view.readStringIdAt(offset);
            errors[offset] = view.readErrorAt(offset);
        }
        return {
            sheetName: request.sheetName,
            rowStart: request.rowStart,
            rowEnd: request.rowEnd,
            col: request.col,
            length: view.length,
            columnVersion: view.columnVersion,
            structureVersion: view.structureVersion,
            sheetColumnVersions: view.sheetColumnVersions,
            tags,
            numbers,
            stringIds,
            errors,
        };
    };
    const getColumnSlice = (request) => {
        const cacheKey = getColumnSliceCacheKey(request.sheetName, request.col, request.rowStart, request.rowEnd);
        const currentSheet = args.state.workbook.getSheet(request.sheetName);
        const currentSheetColumnVersions = currentSheet?.columnVersions ?? emptyColumnVersions;
        const currentColumnVersion = currentSheetColumnVersions[request.col] ?? 0;
        const currentStructureVersion = currentSheet?.structureVersion ?? 0;
        let slice = columnSliceCache.get(cacheKey);
        if (!slice ||
            slice.columnVersion !== currentColumnVersion ||
            slice.structureVersion !== currentStructureVersion ||
            slice.sheetColumnVersions !== currentSheetColumnVersions) {
            slice = buildColumnSlice(request);
            columnSliceCache.set(cacheKey, slice);
        }
        return slice;
    };
    const fillRowMajorValuesFromColumnOwner = (values, owner, rowStart, rowEnd, width, colOffset) => {
        let row = rowStart;
        while (row <= rowEnd) {
            const pageRowStart = Math.floor(row / BLOCK_ROWS) * BLOCK_ROWS;
            const page = owner.pages.get(pageRowStart);
            const pageRowEnd = Math.min(rowEnd, pageRowStart + BLOCK_ROWS - 1);
            for (; row <= pageRowEnd; row += 1) {
                values[(row - rowStart) * width + colOffset] = materializeCellValueFromPage(page, row - pageRowStart);
            }
        }
    };
    const fillRowMajorMatrixFromColumnOwner = (rows, owner, rowStart, rowEnd, colOffset) => {
        let row = rowStart;
        while (row <= rowEnd) {
            const pageRowStart = Math.floor(row / BLOCK_ROWS) * BLOCK_ROWS;
            const page = owner.pages.get(pageRowStart);
            const pageRowEnd = Math.min(rowEnd, pageRowStart + BLOCK_ROWS - 1);
            for (; row <= pageRowEnd; row += 1) {
                rows[row - rowStart][colOffset] = materializeCellValueFromPage(page, row - pageRowStart);
            }
        }
    };
    const findMaxResidentRowInOwner = (owner, rowStart, rowEnd) => {
        let maxResidentRow = -1;
        for (const page of owner.pages.values()) {
            if (page.nonEmptyCount === 0) {
                continue;
            }
            const pageRowEnd = page.rowStart + BLOCK_ROWS - 1;
            if (pageRowEnd < rowStart || page.rowStart > rowEnd) {
                continue;
            }
            const localStart = Math.max(rowStart - page.rowStart, 0);
            const localEnd = Math.min(rowEnd - page.rowStart, BLOCK_ROWS - 1);
            for (let localRow = localEnd; localRow >= localStart; localRow -= 1) {
                if ((page.tags[localRow] ?? 0) !== 0) {
                    maxResidentRow = Math.max(maxResidentRow, page.rowStart + localRow);
                    break;
                }
            }
        }
        return maxResidentRow;
    };
    return {
        reset() {
            normalizedStrings.clear();
            columnOwnerCache.clear();
            columnSliceCache.clear();
        },
        getColumnOwner,
        getColumnView,
        getColumnSlice,
        readCellValue(sheetName, row, col) {
            const view = getColumnView({ sheetName, rowStart: row, rowEnd: row, col });
            return view.readCellValueAt(0);
        },
        readRangeValues({ sheetName, rowStart, rowEnd, colStart, colEnd }) {
            const width = colEnd - colStart + 1;
            const height = rowEnd - rowStart + 1;
            const values = [];
            values.length = width * height;
            for (let col = colStart; col <= colEnd; col += 1) {
                const owner = getColumnOwner({ sheetName, col });
                fillRowMajorValuesFromColumnOwner(values, owner, rowStart, rowEnd, width, col - colStart);
            }
            return values;
        },
        readRangeValueMatrix({ sheetName, rowStart, rowEnd, colStart, colEnd }) {
            const width = colEnd - colStart + 1;
            const height = rowEnd - rowStart + 1;
            const area = width * height;
            const sheet = area <= 16_384 ? args.state.workbook.getSheet(sheetName) : undefined;
            if (sheet?.structureVersion === 1) {
                const rows = Array.from({ length: height }, () => createSparseCellValueRow(width));
                let filledCells = 0;
                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 = sheet.grid.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 = rows[absoluteBlockRow + localRow - rowStart];
                            const blockRowOffset = localRow * BLOCK_COLS;
                            for (let localCol = localColStart; localCol <= localColEnd; localCol += 1) {
                                const encodedCellIndex = block[blockRowOffset + localCol];
                                if (encodedCellIndex === 0) {
                                    continue;
                                }
                                row[absoluteBlockCol + localCol - colStart] = materializeCellValueFromCellIndex(encodedCellIndex - 1);
                                filledCells += 1;
                            }
                        }
                    }
                }
                if (filledCells < area) {
                    for (let rowOffset = 0; rowOffset < height; rowOffset += 1) {
                        const row = rows[rowOffset];
                        for (let colOffset = 0; colOffset < width; colOffset += 1) {
                            row[colOffset] ??= { tag: ValueTag.Empty };
                        }
                    }
                }
                return rows;
            }
            if (sheet) {
                const rows = Array.from({ length: height }, () => []);
                for (let rowOffset = 0; rowOffset < height; rowOffset += 1) {
                    const row = rows[rowOffset];
                    for (let colOffset = 0; colOffset < width; colOffset += 1) {
                        const cellIndex = sheet.grid.get(rowStart + rowOffset, colStart + colOffset);
                        if (cellIndex === -1) {
                            row[colOffset] = { tag: ValueTag.Empty };
                            continue;
                        }
                        row[colOffset] = materializeCellValueFromCellIndex(cellIndex);
                    }
                }
                return rows;
            }
            const rows = Array.from({ length: height }, () => []);
            for (let colOffset = 0; colOffset < width; colOffset += 1) {
                const owner = getColumnOwner({ sheetName, col: colStart + colOffset });
                fillRowMajorMatrixFromColumnOwner(rows, owner, rowStart, rowEnd, colOffset);
            }
            return rows;
        },
        findMaxResidentRowInColumns({ sheetName, rowStart, rowEnd, colStart, colEnd }) {
            if (rowEnd < rowStart || colEnd < colStart) {
                return -1;
            }
            let maxResidentRow = -1;
            for (let col = colStart; col <= colEnd; col += 1) {
                const owner = getColumnOwner({ sheetName, col });
                const ownerMaxResidentRow = findMaxResidentRowInOwner(owner, rowStart, rowEnd);
                if (ownerMaxResidentRow > maxResidentRow) {
                    maxResidentRow = ownerMaxResidentRow;
                }
            }
            return maxResidentRow;
        },
        normalizeStringId,
        normalizeLookupText(value) {
            return (value.stringId !== undefined && value.stringId !== 0 ? normalizeStringId(value.stringId) : value.value).toUpperCase();
        },
    };
}
function createSparseCellValueRow(width) {
    const row = [];
    row.length = width;
    return row;
}
//# sourceMappingURL=runtime-column-store-service.js.map