import { ErrorCode, ValueTag } from '@bilig/protocol';
import { BLOCK_ROWS } from '../sheet-grid.js';
const PREFIX_LITERAL_DELTA_MAX_SUFFIX_LENGTH = 128;
const STALE_PREFIX_VERSION = -1;
function preferAggregateErrorCode(current, incoming) {
    if (incoming === ErrorCode.None) {
        return current;
    }
    if (incoming === ErrorCode.Cycle) {
        return ErrorCode.Cycle;
    }
    return current === ErrorCode.None ? incoming : current;
}
function columnKeyPrefix(sheetName, col) {
    return `${sheetName}\t${col}`;
}
function cacheKey(sheetName, col, rowStart) {
    return `${columnKeyPrefix(sheetName, col)}\t${rowStart}`;
}
function prefixAnchorForRegion(regionRowStart, aggregateKind) {
    return aggregateKind === 'count' ? 0 : regionRowStart;
}
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;
    }
}
function ensureCapacity(entry, totalLength) {
    if (entry.prefixSums.length >= totalLength) {
        return;
    }
    const nextCapacity = Math.max(entry.prefixSums.length * 2, totalLength);
    const nextPrefixSums = new Float64Array(nextCapacity);
    const nextPrefixCount = new Uint32Array(nextCapacity);
    const nextPrefixAverageCount = new Uint32Array(nextCapacity);
    const nextPrefixErrorCodes = new Uint16Array(nextCapacity);
    const nextPrefixErrorCounts = new Uint32Array(nextCapacity);
    const nextPrefixMinimums = new Float64Array(nextCapacity);
    const nextPrefixMaximums = new Float64Array(nextCapacity);
    const currentLength = entry.rowEnd - entry.rowStart + 1;
    nextPrefixSums.set(entry.prefixSums.subarray(0, currentLength), 0);
    nextPrefixCount.set(entry.prefixCount.subarray(0, currentLength), 0);
    nextPrefixAverageCount.set(entry.prefixAverageCount.subarray(0, currentLength), 0);
    nextPrefixErrorCodes.set(entry.prefixErrorCodes.subarray(0, currentLength), 0);
    nextPrefixErrorCounts.set(entry.prefixErrorCounts.subarray(0, currentLength), 0);
    nextPrefixMinimums.set(entry.prefixMinimums.subarray(0, currentLength), 0);
    nextPrefixMaximums.set(entry.prefixMaximums.subarray(0, currentLength), 0);
    entry.prefixSums = nextPrefixSums;
    entry.prefixCount = nextPrefixCount;
    entry.prefixAverageCount = nextPrefixAverageCount;
    entry.prefixErrorCodes = nextPrefixErrorCodes;
    entry.prefixErrorCounts = nextPrefixErrorCounts;
    entry.prefixMinimums = nextPrefixMinimums;
    entry.prefixMaximums = nextPrefixMaximums;
}
function isStaleEntry(entry) {
    return entry.columnVersion === STALE_PREFIX_VERSION && entry.structureVersion === STALE_PREFIX_VERSION;
}
function markEntryStale(entry) {
    entry.columnVersion = STALE_PREFIX_VERSION;
    entry.structureVersion = STALE_PREFIX_VERSION;
    entry.extremaValid = false;
}
function resetEntry(entry, currentVersions) {
    entry.rowEnd = entry.rowStart - 1;
    entry.columnVersion = currentVersions.columnVersion;
    entry.structureVersion = currentVersions.structureVersion;
    entry.extremaValid = true;
}
function isErrorTag(value) {
    return value.tag === ValueTag.Error && value.code !== ErrorCode.None;
}
function numericContribution(value) {
    switch (value.tag) {
        case ValueTag.Number:
            return value.value;
        case ValueTag.Empty:
        case ValueTag.Boolean:
        case ValueTag.String:
        case ValueTag.Error:
        default:
            return 0;
    }
}
function countContribution(value) {
    return value.tag === ValueTag.Number ? 1 : 0;
}
function averageCountContribution(value) {
    return value.tag === ValueTag.Number ? 1 : 0;
}
function emptyWindowSummary() {
    return {
        sum: 0,
        count: 0,
        averageCount: 0,
        minimum: Number.POSITIVE_INFINITY,
        maximum: Number.NEGATIVE_INFINITY,
        errorCount: 0,
        errorCode: ErrorCode.None,
        fullPageHits: 0,
        edgeCellScans: 0,
    };
}
function addRawAggregateContribution(summary, rawTag, numeric, errorCode) {
    switch (decodeValueTag(rawTag)) {
        case ValueTag.Number:
            summary.sum += numeric;
            summary.count += 1;
            summary.averageCount += 1;
            summary.minimum = Math.min(summary.minimum, numeric);
            summary.maximum = Math.max(summary.maximum, numeric);
            break;
        case ValueTag.Error:
            summary.errorCount += 1;
            summary.errorCode = preferAggregateErrorCode(summary.errorCode, errorCode ?? ErrorCode.None);
            break;
        case ValueTag.Empty:
        case ValueTag.Boolean:
        case ValueTag.String:
        default:
            break;
    }
}
function addPageSummary(target, page) {
    if (!page) {
        return;
    }
    target.sum += page.sum;
    target.count += page.count;
    target.averageCount += page.averageCount;
    target.minimum = Math.min(target.minimum, page.minimum);
    target.maximum = Math.max(target.maximum, page.maximum);
    target.errorCount += page.errorCount;
    target.errorCode = preferAggregateErrorCode(target.errorCode, page.errorCode);
}
function summarizePage(page) {
    const summary = emptyWindowSummary();
    for (let localRow = 0; localRow < BLOCK_ROWS; localRow += 1) {
        addRawAggregateContribution(summary, page.tags[localRow], page.numbers[localRow] ?? 0, page.errors[localRow]);
    }
    return {
        sum: summary.sum,
        count: summary.count,
        averageCount: summary.averageCount,
        minimum: summary.minimum,
        maximum: summary.maximum,
        errorCount: summary.errorCount,
        errorCode: summary.errorCode,
    };
}
function getOrSummarizePage(index, owner, pageRowStart) {
    const cached = index.pages.get(pageRowStart);
    if (cached) {
        return cached;
    }
    const page = owner.pages.get(pageRowStart);
    if (!page) {
        return undefined;
    }
    const summary = summarizePage(page);
    index.pages.set(pageRowStart, summary);
    return summary;
}
function scanPageRows(target, page, localStart, localEnd) {
    target.edgeCellScans += localEnd - localStart + 1;
    if (!page) {
        return;
    }
    for (let localRow = localStart; localRow <= localEnd; localRow += 1) {
        addRawAggregateContribution(target, page.tags[localRow], page.numbers[localRow] ?? 0, page.errors[localRow]);
    }
}
export function createAggregateStateStore(args) {
    const cache = new Map();
    const entriesByColumn = new Map();
    const pageIndexCache = new Map();
    const registerEntry = (entry) => {
        const key = columnKeyPrefix(entry.sheetName, entry.col);
        let entries = entriesByColumn.get(key);
        if (!entries) {
            entries = [];
            entriesByColumn.set(key, entries);
        }
        const insertAt = entries.findIndex((candidate) => candidate.rowStart > entry.rowStart);
        if (insertAt === -1) {
            entries.push(entry);
        }
        else {
            entries.splice(insertAt, 0, entry);
        }
    };
    const deleteEntry = (entry) => {
        cache.delete(cacheKey(entry.sheetName, entry.col, entry.rowStart));
        const entries = entriesByColumn.get(columnKeyPrefix(entry.sheetName, entry.col));
        if (!entries) {
            return;
        }
        const index = entries.indexOf(entry);
        if (index !== -1) {
            entries.splice(index, 1);
        }
        if (entries.length === 0) {
            entriesByColumn.delete(columnKeyPrefix(entry.sheetName, entry.col));
        }
    };
    const getCurrentVersions = (sheetName, col) => {
        const sheet = args.workbook.getSheet(sheetName);
        return {
            columnVersion: sheet?.columnVersions[col] ?? 0,
            structureVersion: sheet?.structureVersion ?? 0,
        };
    };
    const getColumnView = (request) => {
        const direct = Reflect.get(args.runtimeColumnStore, 'getColumnView');
        if (typeof direct === 'function') {
            return direct.call(args.runtimeColumnStore, request);
        }
        const slice = args.runtimeColumnStore.getColumnSlice(request);
        return {
            owner: {
                sheetName: slice.sheetName,
                col: slice.col,
                columnVersion: slice.columnVersion,
                structureVersion: slice.structureVersion,
                sheetColumnVersions: slice.sheetColumnVersions,
                pages: new Map(),
            },
            sheetName: slice.sheetName,
            rowStart: slice.rowStart,
            rowEnd: slice.rowEnd,
            col: slice.col,
            length: slice.length,
            columnVersion: slice.columnVersion,
            structureVersion: slice.structureVersion,
            sheetColumnVersions: slice.sheetColumnVersions,
            readTagAt(offset) {
                return slice.tags[offset] ?? ValueTag.Empty;
            },
            readNumberAt(offset) {
                return slice.numbers[offset] ?? 0;
            },
            readStringIdAt(offset) {
                return slice.stringIds[offset] ?? 0;
            },
            readErrorAt(offset) {
                return slice.errors[offset] ?? ErrorCode.None;
            },
            readCellValueAt(offset) {
                return args.runtimeColumnStore.readCellValue(request.sheetName, request.rowStart + offset, request.col);
            },
        };
    };
    const getColumnOwner = (request) => {
        const direct = Reflect.get(args.runtimeColumnStore, 'getColumnOwner');
        return typeof direct === 'function' ? direct.call(args.runtimeColumnStore, request) : undefined;
    };
    const getOrBuildPageIndex = (request) => {
        const owner = getColumnOwner(request);
        if (!owner) {
            return undefined;
        }
        const key = columnKeyPrefix(request.sheetName, request.col);
        const existing = pageIndexCache.get(key);
        if (existing &&
            existing.columnVersion === owner.columnVersion &&
            existing.structureVersion === owner.structureVersion &&
            existing.sheetColumnVersions === owner.sheetColumnVersions) {
            return { index: existing, owner };
        }
        const next = {
            sheetName: request.sheetName,
            col: request.col,
            columnVersion: owner.columnVersion,
            structureVersion: owner.structureVersion,
            sheetColumnVersions: owner.sheetColumnVersions,
            pages: new Map(),
        };
        pageIndexCache.set(key, next);
        return { index: next, owner };
    };
    const summarizeColumnWindow = (request) => {
        if (request.rowEnd < request.rowStart) {
            return {
                sum: 0,
                count: 0,
                averageCount: 0,
                minimum: Number.POSITIVE_INFINITY,
                maximum: Number.NEGATIVE_INFINITY,
                errorCount: 0,
                errorCode: ErrorCode.None,
                fullPageHits: 0,
                edgeCellScans: 0,
            };
        }
        const pageIndex = getOrBuildPageIndex({ sheetName: request.sheetName, col: request.col });
        if (!pageIndex) {
            return undefined;
        }
        const { index, owner } = pageIndex;
        const summary = emptyWindowSummary();
        let row = request.rowStart;
        while (row <= request.rowEnd) {
            const pageRowStart = Math.floor(row / BLOCK_ROWS) * BLOCK_ROWS;
            const pageRowEnd = Math.min(request.rowEnd, pageRowStart + BLOCK_ROWS - 1);
            if (row === pageRowStart && pageRowEnd === pageRowStart + BLOCK_ROWS - 1) {
                addPageSummary(summary, getOrSummarizePage(index, owner, pageRowStart));
                summary.fullPageHits += 1;
            }
            else {
                scanPageRows(summary, owner.pages.get(pageRowStart), row - pageRowStart, pageRowEnd - pageRowStart);
            }
            row = pageRowEnd + 1;
        }
        return summary;
    };
    const hasReusableColumnPrefix = (request, aggregateKind) => {
        const rowStart = prefixAnchorForRegion(request.rowStart, aggregateKind);
        const currentVersions = getCurrentVersions(request.sheetName, request.col);
        const existing = cache.get(cacheKey(request.sheetName, request.col, rowStart));
        return (existing !== undefined &&
            !isStaleEntry(existing) &&
            existing.columnVersion === currentVersions.columnVersion &&
            existing.structureVersion === currentVersions.structureVersion &&
            existing.rowStart <= request.rowStart &&
            (aggregateKind !== 'min' && aggregateKind !== 'max' ? true : existing.extremaValid));
    };
    const extendEntry = (entry, targetRowEnd) => {
        const view = getColumnView({
            sheetName: entry.sheetName,
            rowStart: entry.rowEnd + 1,
            rowEnd: targetRowEnd,
            col: entry.col,
        });
        const currentLength = entry.rowEnd - entry.rowStart + 1;
        const totalLength = targetRowEnd - entry.rowStart + 1;
        ensureCapacity(entry, totalLength);
        let runningSum = currentLength > 0 ? (entry.prefixSums[currentLength - 1] ?? 0) : 0;
        let runningCount = currentLength > 0 ? (entry.prefixCount[currentLength - 1] ?? 0) : 0;
        let runningAverageCount = currentLength > 0 ? (entry.prefixAverageCount[currentLength - 1] ?? 0) : 0;
        let runningErrorCode = currentLength > 0 ? (entry.prefixErrorCodes[currentLength - 1] ?? ErrorCode.None) : ErrorCode.None;
        let runningErrorCount = currentLength > 0 ? (entry.prefixErrorCounts[currentLength - 1] ?? 0) : 0;
        let runningMinimum = currentLength > 0 ? (entry.prefixMinimums[currentLength - 1] ?? Number.POSITIVE_INFINITY) : Number.POSITIVE_INFINITY;
        let runningMaximum = currentLength > 0 ? (entry.prefixMaximums[currentLength - 1] ?? Number.NEGATIVE_INFINITY) : Number.NEGATIVE_INFINITY;
        for (let offset = 0; offset < view.length; offset += 1) {
            const tag = decodeValueTag(view.readTagAt(offset));
            switch (tag) {
                case ValueTag.Number: {
                    const numeric = view.readNumberAt(offset);
                    runningSum += numeric;
                    runningCount += 1;
                    runningAverageCount += 1;
                    runningMinimum = Math.min(runningMinimum, numeric);
                    runningMaximum = Math.max(runningMaximum, numeric);
                    break;
                }
                case ValueTag.Empty:
                case ValueTag.Boolean:
                    break;
                case ValueTag.Error:
                    runningErrorCode = preferAggregateErrorCode(runningErrorCode, view.readErrorAt(offset) ?? ErrorCode.None);
                    runningErrorCount += 1;
                    break;
                case ValueTag.String:
                default:
                    break;
            }
            const targetOffset = currentLength + offset;
            entry.prefixSums[targetOffset] = runningSum;
            entry.prefixCount[targetOffset] = runningCount;
            entry.prefixAverageCount[targetOffset] = runningAverageCount;
            entry.prefixErrorCodes[targetOffset] = runningErrorCode;
            entry.prefixErrorCounts[targetOffset] = runningErrorCount;
            entry.prefixMinimums[targetOffset] = runningMinimum;
            entry.prefixMaximums[targetOffset] = runningMaximum;
        }
        entry.rowEnd = targetRowEnd;
        entry.columnVersion = view.columnVersion;
        entry.structureVersion = view.structureVersion;
        return entry;
    };
    return {
        summarizeColumnWindow,
        hasReusableColumnPrefix,
        getOrBuildPrefixForRegion(regionId, aggregateKind) {
            const region = args.regionGraph.getRegion(regionId);
            if (!region) {
                throw new Error(`Unknown region id: ${regionId}`);
            }
            const rowStart = prefixAnchorForRegion(region.rowStart, aggregateKind);
            const key = cacheKey(region.sheetName, region.col, rowStart);
            const currentVersions = getCurrentVersions(region.sheetName, region.col);
            const existing = cache.get(key);
            if (existing && isStaleEntry(existing)) {
                resetEntry(existing, currentVersions);
                return extendEntry(existing, region.rowEnd);
            }
            if (existing &&
                existing.columnVersion === currentVersions.columnVersion &&
                existing.structureVersion === currentVersions.structureVersion &&
                existing.rowStart <= region.rowStart &&
                (aggregateKind !== 'min' && aggregateKind !== 'max' ? true : existing.extremaValid)) {
                return existing.rowEnd >= region.rowEnd ? existing : extendEntry(existing, region.rowEnd);
            }
            if (existing) {
                deleteEntry(existing);
            }
            const capacity = Math.max(region.rowEnd - rowStart + 1, 16);
            const next = {
                regionId,
                sheetName: region.sheetName,
                col: region.col,
                rowStart,
                rowEnd: rowStart - 1,
                columnVersion: currentVersions.columnVersion,
                structureVersion: currentVersions.structureVersion,
                extremaValid: true,
                prefixSums: new Float64Array(capacity),
                prefixCount: new Uint32Array(capacity),
                prefixAverageCount: new Uint32Array(capacity),
                prefixErrorCodes: new Uint16Array(capacity),
                prefixErrorCounts: new Uint32Array(capacity),
                prefixMinimums: new Float64Array(capacity),
                prefixMaximums: new Float64Array(capacity),
            };
            cache.set(key, next);
            registerEntry(next);
            return extendEntry(next, region.rowEnd);
        },
        noteLiteralWrite({ sheetName, row, col, oldValue, newValue }) {
            pageIndexCache.delete(columnKeyPrefix(sheetName, col));
            const entries = entriesByColumn.get(columnKeyPrefix(sheetName, col));
            if (!entries) {
                return;
            }
            const currentVersions = getCurrentVersions(sheetName, col);
            for (let entryIndex = 0; entryIndex < entries.length; entryIndex += 1) {
                const entry = entries[entryIndex];
                if (entry.rowStart > row) {
                    break;
                }
                if (isStaleEntry(entry)) {
                    continue;
                }
                if (entry.structureVersion !== currentVersions.structureVersion) {
                    deleteEntry(entry);
                    entryIndex -= 1;
                    continue;
                }
                entry.columnVersion = currentVersions.columnVersion;
                entry.structureVersion = currentVersions.structureVersion;
                if (row < entry.rowStart || row > entry.rowEnd) {
                    continue;
                }
                if (isErrorTag(oldValue) || isErrorTag(newValue) || (entry.prefixErrorCounts[entry.rowEnd - entry.rowStart] ?? 0) > 0) {
                    deleteEntry(entry);
                    entryIndex -= 1;
                    continue;
                }
                const sumDelta = numericContribution(newValue) - numericContribution(oldValue);
                const countDelta = countContribution(newValue) - countContribution(oldValue);
                const averageDelta = averageCountContribution(newValue) - averageCountContribution(oldValue);
                if (sumDelta === 0 && countDelta === 0 && averageDelta === 0) {
                    continue;
                }
                const offset = row - entry.rowStart;
                const length = entry.rowEnd - entry.rowStart + 1;
                if (length - offset > PREFIX_LITERAL_DELTA_MAX_SUFFIX_LENGTH) {
                    markEntryStale(entry);
                    continue;
                }
                for (let index = offset; index < length; index += 1) {
                    entry.prefixSums[index] = (entry.prefixSums[index] ?? 0) + sumDelta;
                    entry.prefixCount[index] = (entry.prefixCount[index] ?? 0) + countDelta;
                    entry.prefixAverageCount[index] = (entry.prefixAverageCount[index] ?? 0) + averageDelta;
                }
                entry.extremaValid = false;
            }
        },
        invalidateColumn(sheetName, col) {
            const key = columnKeyPrefix(sheetName, col);
            pageIndexCache.delete(key);
            const entries = entriesByColumn.get(key);
            if (!entries) {
                return;
            }
            entries.forEach((entry) => {
                cache.delete(cacheKey(entry.sheetName, entry.col, entry.rowStart));
            });
            entriesByColumn.delete(key);
        },
    };
}
//# sourceMappingURL=aggregate-state-store.js.map