import { ErrorCode, ValueTag } from '@bilig/protocol';
import { addEngineCounter } from '../../perf/engine-counters.js';
import { BLOCK_COLS, BLOCK_ROWS } from '../../sheet-grid.js';
// Native marshaling loses below the existing JS direct-evaluation limit; use it only to avoid the over-limit recalc path.
const NATIVE_INITIAL_PREFIX_AGGREGATE_MIN_FORMULAS = 16_385;
const NATIVE_INITIAL_PREFIX_AGGREGATE_MAX_VALUES = 2_000_000;
const NATIVE_DIRECT_AGGREGATE_OP_SUM = 1;
const NATIVE_DIRECT_AGGREGATE_OP_AVERAGE = 2;
const NATIVE_DIRECT_AGGREGATE_OP_COUNT = 3;
const NATIVE_DIRECT_AGGREGATE_OP_MIN = 4;
const NATIVE_DIRECT_AGGREGATE_OP_MAX = 5;
function preferAggregateErrorCode(current, incoming) {
    if (incoming === ErrorCode.None) {
        return current;
    }
    if (incoming === ErrorCode.Cycle) {
        return ErrorCode.Cycle;
    }
    return current === ErrorCode.None ? incoming : current;
}
function isInitialFormulaMember(args, cellIndex) {
    return args.state.formulas.has(cellIndex) || ((args.state.workbook.cellStore.flags[cellIndex] ?? 0) & 2 /* CellFlags.HasFormula */) !== 0;
}
export function canEvaluateInitialPrefixAggregateGroupsNatively(args, orderedCellIndices, options) {
    const collected = collectInitialPrefixAggregateGroups(args, orderedCellIndices);
    if (!collected || collected.groups.size === 0) {
        return false;
    }
    if (options?.requireAllCells === true && collected.matchedFormulaCount !== orderedCellIndices.length) {
        return false;
    }
    for (const group of collected.groups.values()) {
        if (!canUseNativeInitialPrefixAggregateGroup(args, group)) {
            return false;
        }
    }
    return true;
}
export function evaluateInitialPrefixAggregateGroups(args, orderedCellIndices, pushChangedCellIndex, writeFormulaValue) {
    const collected = collectInitialPrefixAggregateGroups(args, orderedCellIndices);
    if (!collected || collected.groups.size === 0) {
        return undefined;
    }
    const handled = new Set();
    collected.groups.forEach((group) => {
        if (tryEvaluateNativeInitialPrefixAggregateGroup(args, group, handled, pushChangedCellIndex, writeFormulaValue)) {
            return;
        }
        evaluateInitialPrefixAggregateGroupInJs(args, group, handled, pushChangedCellIndex, writeFormulaValue);
    });
    return handled.size === 0 ? undefined : handled;
}
function collectInitialPrefixAggregateGroups(args, orderedCellIndices) {
    const groups = new Map();
    let matchedFormulaCount = 0;
    for (let index = 0; index < orderedCellIndices.length; index += 1) {
        args.checkEvaluationBudget();
        const cellIndex = orderedCellIndices[index];
        const formula = args.state.formulas.get(cellIndex);
        const aggregate = formula?.directAggregate;
        if (!formula || !aggregate || aggregate.rowStart !== 0 || formula.dependencyIndices.length !== 0) {
            continue;
        }
        matchedFormulaCount += 1;
        const key = `${aggregate.sheetName}\t${aggregate.col}\t${aggregate.colEnd}\t${aggregate.aggregateKind}`;
        let group = groups.get(key);
        if (!group) {
            group = {
                sheetName: aggregate.sheetName,
                col: aggregate.col,
                colEnd: aggregate.colEnd,
                aggregateKind: aggregate.aggregateKind,
                maxRowEnd: aggregate.rowEnd,
                lastRowEnd: aggregate.rowEnd,
                formulasAreOrdered: true,
                formulas: [],
            };
            groups.set(key, group);
        }
        else {
            group.maxRowEnd = Math.max(group.maxRowEnd, aggregate.rowEnd);
            if (aggregate.rowEnd < group.lastRowEnd) {
                group.formulasAreOrdered = false;
            }
            group.lastRowEnd = aggregate.rowEnd;
        }
        group.formulas.push({
            cellIndex,
            rowEnd: aggregate.rowEnd,
            ...(aggregate.resultOffset !== undefined ? { resultOffset: aggregate.resultOffset } : {}),
        });
    }
    return groups.size === 0 ? undefined : { groups, matchedFormulaCount };
}
function canUseNativeInitialPrefixAggregateGroup(args, group) {
    if (group.formulas.length < NATIVE_INITIAL_PREFIX_AGGREGATE_MIN_FORMULAS || group.colEnd < group.col) {
        return false;
    }
    const sheet = args.state.workbook.getSheet(group.sheetName);
    if (!sheet || !args.state.wasm.initSyncIfPossible()) {
        return false;
    }
    const rowCount = group.maxRowEnd + 1;
    const colCount = group.colEnd - group.col + 1;
    const valueCount = rowCount * colCount;
    if (rowCount <= 0 ||
        colCount <= 0 ||
        !Number.isSafeInteger(valueCount) ||
        valueCount <= 0 ||
        valueCount > NATIVE_INITIAL_PREFIX_AGGREGATE_MAX_VALUES) {
        return false;
    }
    for (let row = 0; row <= group.maxRowEnd; row += 1) {
        args.checkEvaluationBudget();
        for (let col = group.col; col <= group.colEnd; col += 1) {
            const memberCellIndex = sheet.structureVersion === 1 ? sheet.grid.getPhysical(row, col) : sheet.grid.get(row, col);
            if (memberCellIndex !== -1 && isInitialFormulaMember(args, memberCellIndex)) {
                return false;
            }
        }
    }
    return true;
}
function tryEvaluateNativeInitialPrefixAggregateGroup(args, group, handled, pushChangedCellIndex, writeFormulaValue) {
    if (!canUseNativeInitialPrefixAggregateGroup(args, group)) {
        return false;
    }
    const sheet = args.state.workbook.getSheet(group.sheetName);
    if (!sheet) {
        return false;
    }
    const formulas = orderedGroupFormulas(group);
    const rowCount = group.maxRowEnd + 1;
    const colCount = group.colEnd - group.col + 1;
    const valueCount = rowCount * colCount;
    const tags = new Uint8Array(valueCount);
    const numbers = new Float64Array(valueCount);
    const errors = new Uint16Array(valueCount);
    let offset = 0;
    for (let row = 0; row < rowCount; row += 1) {
        args.checkEvaluationBudget();
        for (let col = group.col; col <= group.colEnd; col += 1) {
            const memberCellIndex = sheet.structureVersion === 1 ? sheet.grid.getPhysical(row, col) : sheet.grid.get(row, col);
            if (memberCellIndex === -1) {
                tags[offset] = ValueTag.Empty;
            }
            else {
                if (isInitialFormulaMember(args, memberCellIndex)) {
                    return false;
                }
                tags[offset] = args.state.workbook.cellStore.tags[memberCellIndex] ?? ValueTag.Empty;
                numbers[offset] = args.state.workbook.cellStore.numbers[memberCellIndex] ?? 0;
                errors[offset] = args.state.workbook.cellStore.errors[memberCellIndex] ?? ErrorCode.None;
            }
            offset += 1;
        }
    }
    const formulaRowEnds = new Uint32Array(formulas.length);
    const resultOffsets = new Float64Array(formulas.length);
    for (let index = 0; index < formulas.length; index += 1) {
        formulaRowEnds[index] = formulas[index].rowEnd;
        resultOffsets[index] = formulas[index].resultOffset ?? 0;
    }
    const outTags = new Uint8Array(formulas.length);
    const outNumbers = new Float64Array(formulas.length);
    const outErrors = new Uint16Array(formulas.length);
    if (!args.state.wasm.evalAnchoredPrefixAggregateBatch({
        aggregateKind: nativeDirectAggregateKind(group.aggregateKind),
        tags,
        numbers,
        errors,
        rowCount,
        colCount,
        formulaRowEnds,
        resultOffsets,
        outTags,
        outNumbers,
        outErrors,
    })) {
        return false;
    }
    for (let index = 0; index < formulas.length; index += 1) {
        const formula = formulas[index];
        const tag = outTags[index] ?? ValueTag.Empty;
        const value = tag === ValueTag.Number
            ? { tag: ValueTag.Number, value: outNumbers[index] ?? 0 }
            : tag === ValueTag.Error
                ? { tag: ValueTag.Error, code: outErrors[index] ?? ErrorCode.None }
                : { tag: ValueTag.Empty };
        writeFormulaValue(formula.cellIndex, value);
        handled.add(formula.cellIndex);
        pushChangedCellIndex(formula.cellIndex);
    }
    addEngineCounter(args.state.counters, 'nativeDirectAggregatePrefixEvaluations', formulas.length);
    return true;
}
function evaluateInitialPrefixAggregateGroupInJs(args, group, handled, pushChangedCellIndex, writeFormulaValue) {
    if (tryEvaluatePhysicalSingleColumnPrefixAggregateGroupInJs(args, group, handled, pushChangedCellIndex, writeFormulaValue)) {
        return;
    }
    const sheet = args.state.workbook.getSheet(group.sheetName);
    if (!sheet) {
        return;
    }
    const formulas = orderedGroupFormulas(group);
    let sum = 0;
    let count = 0;
    let averageCount = 0;
    let errorCode = ErrorCode.None;
    let errorCount = 0;
    let minimum = Number.POSITIVE_INFINITY;
    let maximum = Number.NEGATIVE_INFINITY;
    let formulaIndex = 0;
    let encounteredFormulaMember = false;
    for (let row = 0; row <= group.maxRowEnd && !encounteredFormulaMember; row += 1) {
        args.checkEvaluationBudget();
        for (let col = group.col; col <= group.colEnd; col += 1) {
            const memberCellIndex = sheet.structureVersion === 1 ? sheet.grid.getPhysical(row, col) : sheet.grid.get(row, col);
            if (memberCellIndex !== -1) {
                if (isInitialFormulaMember(args, memberCellIndex)) {
                    encounteredFormulaMember = true;
                    break;
                }
                const tag = args.state.workbook.cellStore.tags[memberCellIndex] ?? ValueTag.Empty;
                if (tag === ValueTag.Number) {
                    const numeric = args.state.workbook.cellStore.numbers[memberCellIndex] ?? 0;
                    sum += numeric;
                    count += 1;
                    averageCount += 1;
                    minimum = Math.min(minimum, numeric);
                    maximum = Math.max(maximum, numeric);
                }
                else if (tag === ValueTag.Boolean || tag === ValueTag.Empty) {
                    // Reference aggregates ignore logical and blank cells.
                }
                else if (tag === ValueTag.Error) {
                    errorCode = preferAggregateErrorCode(errorCode, args.state.workbook.cellStore.errors[memberCellIndex] ?? ErrorCode.None);
                    errorCount += 1;
                }
            }
        }
        while (formulaIndex < formulas.length && formulas[formulaIndex].rowEnd <= row) {
            const formula = formulas[formulaIndex];
            const aggregateValue = aggregateValueFromState(group.aggregateKind, sum, count, averageCount, errorCode, errorCount, minimum, maximum);
            const value = formula.resultOffset !== undefined && aggregateValue.tag === ValueTag.Number
                ? { tag: ValueTag.Number, value: aggregateValue.value + formula.resultOffset }
                : aggregateValue;
            writeFormulaValue(formula.cellIndex, value);
            handled.add(formula.cellIndex);
            pushChangedCellIndex(formula.cellIndex);
            formulaIndex += 1;
        }
    }
}
function tryEvaluatePhysicalSingleColumnPrefixAggregateGroupInJs(args, group, handled, pushChangedCellIndex, writeFormulaValue) {
    if (group.col !== group.colEnd ||
        (group.aggregateKind !== 'sum' && group.aggregateKind !== 'count' && group.aggregateKind !== 'average')) {
        return false;
    }
    const sheet = args.state.workbook.getSheet(group.sheetName);
    if (!sheet || sheet.structureVersion !== 1) {
        return false;
    }
    const formulas = orderedGroupFormulas(group);
    if (formulas.length === 1 && formulas[0].rowEnd === group.maxRowEnd) {
        return tryEvaluatePhysicalSingleColumnTerminalPrefixAggregateInJs(args, group, formulas[0], handled, pushChangedCellIndex, writeFormulaValue);
    }
    const computedValues = [];
    let formulaIndex = 0;
    let sum = 0;
    let count = 0;
    let averageCount = 0;
    let errorCode = ErrorCode.None;
    let errorCount = 0;
    let encounteredFormulaMember = false;
    const emitThroughRow = (row) => {
        while (formulaIndex < formulas.length && formulas[formulaIndex].rowEnd <= row) {
            const formula = formulas[formulaIndex];
            const aggregateValue = aggregateValueFromState(group.aggregateKind, sum, count, averageCount, errorCode, errorCount, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY);
            const value = formula.resultOffset !== undefined && aggregateValue.tag === ValueTag.Number
                ? { tag: ValueTag.Number, value: aggregateValue.value + formula.resultOffset }
                : aggregateValue;
            computedValues.push(value);
            formulaIndex += 1;
        }
    };
    const blockCol = Math.floor(group.col / BLOCK_COLS);
    const localCol = group.col - blockCol * BLOCK_COLS;
    let row = 0;
    while (row <= group.maxRowEnd && !encounteredFormulaMember) {
        const blockRow = Math.floor(row / BLOCK_ROWS);
        const blockRowStart = blockRow * BLOCK_ROWS;
        const blockRowEnd = Math.min(group.maxRowEnd, blockRowStart + BLOCK_ROWS - 1);
        const block = sheet.grid.blocks.get(blockRow * 1_000_000 + blockCol);
        if (!block) {
            row = blockRowEnd + 1;
            continue;
        }
        for (let localRow = row - blockRowStart; localRow <= blockRowEnd - blockRowStart; localRow += 1) {
            const memberCellValue = block[localRow * BLOCK_COLS + localCol];
            if (memberCellValue === 0) {
                continue;
            }
            const memberCellIndex = memberCellValue - 1;
            const memberRow = blockRowStart + localRow;
            args.checkEvaluationBudget();
            emitThroughRow(memberRow - 1);
            if (isInitialFormulaMember(args, memberCellIndex)) {
                encounteredFormulaMember = true;
                break;
            }
            const tag = args.state.workbook.cellStore.tags[memberCellIndex] ?? ValueTag.Empty;
            if (tag === ValueTag.Number) {
                const numeric = args.state.workbook.cellStore.numbers[memberCellIndex] ?? 0;
                sum += numeric;
                count += 1;
                averageCount += 1;
            }
            else if (tag === ValueTag.Boolean) {
                // Reference aggregates ignore logical cells.
            }
            else if (tag === ValueTag.Error) {
                errorCode = preferAggregateErrorCode(errorCode, args.state.workbook.cellStore.errors[memberCellIndex] ?? ErrorCode.None);
                errorCount += 1;
            }
            emitThroughRow(memberRow);
        }
        row = blockRowEnd + 1;
    }
    if (encounteredFormulaMember) {
        return false;
    }
    emitThroughRow(group.maxRowEnd);
    if (computedValues.length !== formulas.length) {
        return false;
    }
    for (let index = 0; index < formulas.length; index += 1) {
        const formula = formulas[index];
        writeFormulaValue(formula.cellIndex, computedValues[index]);
        handled.add(formula.cellIndex);
        pushChangedCellIndex(formula.cellIndex);
    }
    return true;
}
function tryEvaluatePhysicalSingleColumnTerminalPrefixAggregateInJs(args, group, formula, handled, pushChangedCellIndex, writeFormulaValue) {
    let sum = 0;
    let count = 0;
    let averageCount = 0;
    let errorCode = ErrorCode.None;
    let errorCount = 0;
    const sheet = args.state.workbook.getSheet(group.sheetName);
    if (!sheet || sheet.structureVersion !== 1) {
        return false;
    }
    const blockCol = Math.floor(group.col / BLOCK_COLS);
    const localCol = group.col - blockCol * BLOCK_COLS;
    let row = 0;
    while (row <= formula.rowEnd) {
        const blockRow = Math.floor(row / BLOCK_ROWS);
        const blockRowStart = blockRow * BLOCK_ROWS;
        const blockRowEnd = Math.min(formula.rowEnd, blockRowStart + BLOCK_ROWS - 1);
        const block = sheet.grid.blocks.get(blockRow * 1_000_000 + blockCol);
        if (!block) {
            row = blockRowEnd + 1;
            continue;
        }
        for (let localRow = row - blockRowStart; localRow <= blockRowEnd - blockRowStart; localRow += 1) {
            const memberCellValue = block[localRow * BLOCK_COLS + localCol];
            if (memberCellValue === 0) {
                continue;
            }
            const memberCellIndex = memberCellValue - 1;
            args.checkEvaluationBudget();
            if (isInitialFormulaMember(args, memberCellIndex)) {
                return false;
            }
            const tag = args.state.workbook.cellStore.tags[memberCellIndex] ?? ValueTag.Empty;
            if (tag === ValueTag.Number) {
                sum += args.state.workbook.cellStore.numbers[memberCellIndex] ?? 0;
                count += 1;
                averageCount += 1;
            }
            else if (tag === ValueTag.Boolean) {
                // Reference aggregates ignore logical cells.
            }
            else if (tag === ValueTag.Error) {
                errorCode = preferAggregateErrorCode(errorCode, args.state.workbook.cellStore.errors[memberCellIndex] ?? ErrorCode.None);
                errorCount += 1;
            }
        }
        row = blockRowEnd + 1;
    }
    const aggregateValue = aggregateValueFromState(group.aggregateKind, sum, count, averageCount, errorCode, errorCount, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY);
    const value = formula.resultOffset !== undefined && aggregateValue.tag === ValueTag.Number
        ? { tag: ValueTag.Number, value: aggregateValue.value + formula.resultOffset }
        : aggregateValue;
    writeFormulaValue(formula.cellIndex, value);
    handled.add(formula.cellIndex);
    pushChangedCellIndex(formula.cellIndex);
    return true;
}
function orderedGroupFormulas(group) {
    return group.formulasAreOrdered ? group.formulas : group.formulas.toSorted((left, right) => left.rowEnd - right.rowEnd);
}
function aggregateValueFromState(aggregateKind, sum, count, averageCount, errorCode, errorCount, minimum, maximum) {
    return aggregateKind === 'sum'
        ? errorCount > 0 && errorCode !== ErrorCode.None
            ? { tag: ValueTag.Error, code: errorCode }
            : { tag: ValueTag.Number, value: sum }
        : aggregateKind === 'count'
            ? { tag: ValueTag.Number, value: count }
            : aggregateKind === 'average'
                ? errorCount > 0 && errorCode !== ErrorCode.None
                    ? { tag: ValueTag.Error, code: errorCode }
                    : averageCount === 0
                        ? { tag: ValueTag.Error, code: ErrorCode.Div0 }
                        : { tag: ValueTag.Number, value: sum / averageCount }
                : errorCount > 0 && errorCode !== ErrorCode.None
                    ? { tag: ValueTag.Error, code: errorCode }
                    : aggregateKind === 'min'
                        ? { tag: ValueTag.Number, value: minimum === Number.POSITIVE_INFINITY ? 0 : minimum }
                        : { tag: ValueTag.Number, value: maximum === Number.NEGATIVE_INFINITY ? 0 : maximum };
}
function nativeDirectAggregateKind(kind) {
    switch (kind) {
        case 'sum':
            return NATIVE_DIRECT_AGGREGATE_OP_SUM;
        case 'average':
            return NATIVE_DIRECT_AGGREGATE_OP_AVERAGE;
        case 'count':
            return NATIVE_DIRECT_AGGREGATE_OP_COUNT;
        case 'min':
            return NATIVE_DIRECT_AGGREGATE_OP_MIN;
        case 'max':
            return NATIVE_DIRECT_AGGREGATE_OP_MAX;
    }
}
//# sourceMappingURL=formula-initialization-prefix-aggregates.js.map