import { Effect } from 'effect';
import { ErrorCode, MAX_COLS, MAX_ROWS, ValueTag } from '@bilig/protocol';
import { createLookupBuiltinResolver, evaluatePlanResult, evaluatePlanScalarResult, formatAddress, lowerToPlan, isArrayValue, parseFormula, scalarFromEvaluationResult, parseCellAddress, parseRangeAddress, } from '@bilig/formula';
import { definedNameValueToCellValue, definedNameValueToReferenceOperand } from '../../engine-metadata-utils.js';
import { emptyValue, errorValue } from '../../engine-value-utils.js';
import { addEngineCounter } from '../../perf/engine-counters.js';
import { EngineFormulaEvaluationError } from '../errors.js';
import { directCriteriaRangeVersionKey, rememberDirectCriteriaResult } from './formula-evaluation-direct-criteria-cache.js';
import { applyDirectCriteriaResultTransforms, numericLikeValueInView, strictNumericAggregateCandidateInView, tryEvaluateDirectCriteriaTransformShortCircuit, } from './formula-evaluation-direct-criteria-transforms.js';
import { tryEvaluateDirectVectorLookup } from './formula-evaluation-direct-lookup.js';
import { tryEvaluateDirectIndexExactMatch, tryEvaluateDirectIndexOffset } from './formula-evaluation-direct-index.js';
import { tryEvaluateDirectScalar } from './formula-evaluation-direct-scalar.js';
import { getRuntimeFormulaStructuralCompiled } from '../runtime-formula-source.js';
import { cellValuesEqual, directErrorResult, directNumberResult, evaluationErrorMessage, referenceReplacementKey, } from './formula-evaluation-helpers.js';
import { tryEvaluateDirectAggregate } from './formula-evaluation-direct-aggregate.js';
import { tryEvaluateNativeDirectCriteriaMatchedAggregate, tryEvaluateNativeDirectCriteriaPredicateAggregate, } from './formula-evaluation-direct-criteria-native.js';
import { createDirectCriteriaSharingContext } from './formula-evaluation-direct-criteria-sharing.js';
import { createRowVisibilityResolvers } from './formula-evaluation-row-hidden.js';
import { readPrecisionAsDisplayedCellValue, roundFormulaResultForPrecisionAsDisplayed } from './precision-as-displayed.js';
import { resolveStructuredReferenceNow } from './formula-evaluation-structured-reference.js';
import { tryEvaluateFormulaLeafInlineScalar } from './formula-leaf-inline-scalar-evaluator.js';
const DIRECT_CRITERIA_MATCH_CACHE_LIMIT = 16_384;
const INDEXED_WHOLE_AXIS_BOUND_LIMIT = 4096;
export function createEngineFormulaEvaluationService(args) {
    const emptyChangedCellIndices = [];
    const directCriteriaAggregateCache = new Map();
    const directCriteriaMatchCache = new Map();
    const nativeDirectCriteriaPredicateLayoutCache = new Map();
    const rememberDirectCriteriaMatch = (key, value) => {
        if (directCriteriaMatchCache.size >= DIRECT_CRITERIA_MATCH_CACHE_LIMIT) {
            const firstKey = directCriteriaMatchCache.keys().next().value;
            if (firstKey !== undefined) {
                directCriteriaMatchCache.delete(firstKey);
            }
        }
        directCriteriaMatchCache.set(key, value);
        return value;
    };
    const readCellValue = (sheetName, address) => {
        if (!args.state.workbook.getSheet(sheetName)) {
            return errorValue(ErrorCode.Ref);
        }
        const parsed = parseCellAddress(address, sheetName);
        return readCellValueAt(sheetName, parsed.row, parsed.col);
    };
    const readCellValueByIndex = (cellIndex) => {
        return readPrecisionAsDisplayedCellValue(args.state, cellIndex);
    };
    const readCellValueAt = (sheetName, row, col) => {
        const sheet = args.state.workbook.getSheet(sheetName);
        return sheet ? readCellValueByIndex(sheet.logical.getVisibleCell(row, col)) : errorValue(ErrorCode.Ref);
    };
    const workbookDateSystem = () => args.state.workbook.getCalculationSettings().dateSystem ?? '1900';
    const directVectorLookupContext = {
        state: args.state,
        exactLookup: args.exactLookup,
        sortedLookup: args.sortedLookup,
        readCellValueByIndex,
    };
    const readRectangularRangeValues = (sheetName, bounds, replacements, visiting) => {
        if (bounds.rowEnd < bounds.rowStart || bounds.colEnd < bounds.colStart) {
            return [];
        }
        const cellCount = (bounds.rowEnd - bounds.rowStart + 1) * (bounds.colEnd - bounds.colStart + 1);
        args.checkEvaluationBudget(cellCount);
        if (!replacements || !visiting) {
            if (cellCount <= 64) {
                const values = [];
                for (let row = bounds.rowStart; row <= bounds.rowEnd; row += 1) {
                    for (let col = bounds.colStart; col <= bounds.colEnd; col += 1) {
                        values.push(readCellValueAt(sheetName, row, col));
                    }
                }
                return values;
            }
            const rangeValues = args.runtimeColumnStore.readRangeValues({
                sheetName,
                rowStart: bounds.rowStart,
                rowEnd: bounds.rowEnd,
                colStart: bounds.colStart,
                colEnd: bounds.colEnd,
            });
            args.checkEvaluationBudget(rangeValues.length);
            return rangeValues;
        }
        const values = [];
        for (let row = bounds.rowStart; row <= bounds.rowEnd; row += 1) {
            for (let col = bounds.colStart; col <= bounds.colEnd; col += 1) {
                args.checkEvaluationBudget();
                values.push(evaluateCellWithReferenceReplacements(sheetName, formatAddress(row, col), replacements, visiting));
            }
        }
        return values;
    };
    const readRangeValues = (sheetName, start, end, refKind, replacements, visiting) => {
        const sheet = args.state.workbook.getSheet(sheetName);
        if (!sheet) {
            return [errorValue(ErrorCode.Ref)];
        }
        const range = parseRangeAddress(`${start}:${end}`, sheetName);
        if (range.kind === 'cells' && refKind === 'cells') {
            return readRectangularRangeValues(sheetName, {
                rowStart: range.start.row,
                rowEnd: range.end.row,
                colStart: range.start.col,
                colEnd: range.end.col,
            }, replacements, visiting);
        }
        if (range.kind === 'rows' && refKind === 'rows') {
            const rowStart = Math.max(0, range.start.row);
            const rowEnd = Math.min(MAX_ROWS - 1, range.end.row);
            if (rowEnd < rowStart) {
                return [];
            }
            let maxResidentCol = -1;
            if (rowEnd - rowStart + 1 <= INDEXED_WHOLE_AXIS_BOUND_LIMIT) {
                for (let row = rowStart; row <= rowEnd; row += 1) {
                    sheet.logical.forEachVisibleRowCellEntry(row, (_cellIndex, col) => {
                        if (col >= 0 && col < MAX_COLS && col > maxResidentCol) {
                            maxResidentCol = col;
                        }
                    });
                }
            }
            else {
                sheet.grid.forEachCellEntry((_cellIndex, row, col) => {
                    if (row >= rowStart && row <= rowEnd && col >= 0 && col < MAX_COLS && col > maxResidentCol) {
                        maxResidentCol = col;
                    }
                });
            }
            return maxResidentCol < 0
                ? []
                : readRectangularRangeValues(sheetName, {
                    rowStart,
                    rowEnd,
                    colStart: 0,
                    colEnd: maxResidentCol,
                }, replacements, visiting);
        }
        if (range.kind === 'cols' && refKind === 'cols') {
            const colStart = Math.max(0, range.start.col);
            const colEnd = Math.min(MAX_COLS - 1, range.end.col);
            if (colEnd < colStart) {
                return [];
            }
            let maxResidentRow = -1;
            if (colEnd - colStart + 1 <= INDEXED_WHOLE_AXIS_BOUND_LIMIT) {
                maxResidentRow = args.runtimeColumnStore.findMaxResidentRowInColumns({
                    sheetName,
                    rowStart: 0,
                    rowEnd: MAX_ROWS - 1,
                    colStart,
                    colEnd,
                });
            }
            else {
                sheet.grid.forEachCellEntry((_cellIndex, row, col) => {
                    if (col >= colStart && col <= colEnd && row >= 0 && row < MAX_ROWS && row > maxResidentRow) {
                        maxResidentRow = row;
                    }
                });
            }
            return maxResidentRow < 0
                ? []
                : readRectangularRangeValues(sheetName, {
                    rowStart: 0,
                    rowEnd: maxResidentRow,
                    colStart,
                    colEnd,
                }, replacements, visiting);
        }
        return [];
    };
    const resolveIndexedExactMatch = (lookupValue, range) => {
        if (!args.state.getUseColumnIndex() || range.refKind !== 'cells' || range.cols !== 1) {
            return undefined;
        }
        if (!range.sheetName || !range.start || !range.end) {
            return undefined;
        }
        const result = args.exactLookup.findVectorMatch({
            lookupValue,
            sheetName: range.sheetName,
            start: range.start,
            end: range.end,
            searchMode: 1,
        });
        return result.handled ? result.position : undefined;
    };
    let lookupBuiltinResolver;
    const resolveLookupBuiltin = (name) => {
        lookupBuiltinResolver ??= createLookupBuiltinResolver({
            resolveIndexedExactMatch,
        });
        return lookupBuiltinResolver(name);
    };
    const resolveExactVectorMatch = (_formula, request) => {
        return args.exactLookup.findVectorMatch(request);
    };
    const resolveApproximateVectorMatch = (_formula, request) => {
        return args.sortedLookup.findVectorMatch(request);
    };
    const directCriteriaSharing = createDirectCriteriaSharingContext({ state: args.state, readCellValueByIndex });
    const tryEvaluateDirectCriteriaAggregate = (formula, cellIndex) => {
        const directCriteria = formula.directCriteria;
        if (!directCriteria)
            return undefined;
        const transformShortCircuit = tryEvaluateDirectCriteriaTransformShortCircuit(readCellValueByIndex, formula);
        if (transformShortCircuit) {
            return transformShortCircuit;
        }
        const directIndexOffsetResult = tryEvaluateDirectIndexOffset({
            directCriteria,
            runtimeColumnStore: args.runtimeColumnStore,
            readCellValueByIndex,
            ownerRow: args.state.workbook.cellStore.rows[cellIndex],
        });
        if (directIndexOffsetResult !== undefined) {
            return applyDirectCriteriaResultTransforms(readCellValueByIndex, formula, directIndexOffsetResult);
        }
        const directCriteriaPairs = directCriteriaSharing.resolveDirectCriteriaPairs(formula);
        if (directCriteriaPairs?.error !== undefined) {
            return directCriteriaPairs.error;
        }
        const resolvedPairs = directCriteriaPairs?.pairs ?? [];
        const aggregateRange = directCriteria.aggregateRange;
        const criteriaVersionKey = directCriteriaSharing.directCriteriaVersionKeyForPairs(resolvedPairs);
        const aggregateCacheKey = aggregateRange === undefined
            ? directCriteria.aggregateKind === 'count'
                ? [directCriteria.aggregateKind, criteriaVersionKey].join('\u0000')
                : undefined
            : [directCriteria.aggregateKind, directCriteriaRangeVersionKey(args.state, aggregateRange), criteriaVersionKey].join('\u0000');
        const cachedAggregate = aggregateCacheKey === undefined ? undefined : directCriteriaAggregateCache.get(aggregateCacheKey);
        if (cachedAggregate) {
            addEngineCounter(args.state.counters, 'directCriteriaAggregateCacheHits');
            return applyDirectCriteriaResultTransforms(readCellValueByIndex, formula, cachedAggregate);
        }
        const applyCachedAggregateResult = (value) => applyDirectCriteriaResultTransforms(readCellValueByIndex, formula, aggregateCacheKey === undefined ? value : rememberDirectCriteriaResult(directCriteriaAggregateCache, aggregateCacheKey, value));
        const directIndexExactMatchResult = resolvedPairs.length === 1
            ? tryEvaluateDirectIndexExactMatch({
                directCriteria,
                exactLookup: args.exactLookup,
                runtimeColumnStore: args.runtimeColumnStore,
                lookupValue: resolvedPairs[0].criteria,
            })
            : undefined;
        if (directIndexExactMatchResult !== undefined) {
            return applyDirectCriteriaResultTransforms(readCellValueByIndex, formula, directIndexExactMatchResult);
        }
        const exactCriteriaAggregateResult = directCriteria.aggregateKind !== 'first'
            ? args.criterionCache.getOrBuildExactCriteriaAggregate({
                criteriaPairs: resolvedPairs,
                ...(aggregateRange === undefined ? {} : { aggregateRange }),
                aggregateKind: directCriteria.aggregateKind,
                useCompoundBucketIndex: directCriteriaSharing.shouldUseCompoundExactBucketIndex(resolvedPairs),
            })
            : undefined;
        if (exactCriteriaAggregateResult !== undefined) {
            return applyCachedAggregateResult(exactCriteriaAggregateResult);
        }
        const cachedMatches = directCriteriaMatchCache.get(criteriaVersionKey);
        if (cachedMatches !== undefined) {
            addEngineCounter(args.state.counters, 'directCriteriaMatchCacheHits');
        }
        const shouldShareCriteriaMatches = directCriteria.aggregateKind !== 'first' &&
            cachedMatches === undefined &&
            directCriteriaSharing.directCriteriaShareCount(criteriaVersionKey) > 1;
        let matches = cachedMatches ??
            (shouldShareCriteriaMatches
                ? args.criterionCache.getOrBuildMatchingRows({
                    criteriaPairs: resolvedPairs,
                })
                : undefined);
        if (matches !== undefined && !('tag' in matches) && cachedMatches === undefined) {
            rememberDirectCriteriaMatch(criteriaVersionKey, matches);
        }
        if (matches === undefined) {
            const indexedPredicateAggregateResult = directCriteria.aggregateKind !== 'first'
                ? args.criterionCache.getOrBuildIndexedPredicateAggregate({
                    criteriaPairs: resolvedPairs,
                    ...(aggregateRange === undefined ? {} : { aggregateRange }),
                    aggregateKind: directCriteria.aggregateKind,
                })
                : undefined;
            if (indexedPredicateAggregateResult !== undefined) {
                return applyCachedAggregateResult(indexedPredicateAggregateResult);
            }
            const nativePredicateAggregateResult = tryEvaluateNativeDirectCriteriaPredicateAggregate({
                state: args.state,
                runtimeColumnStore: args.runtimeColumnStore,
            }, {
                aggregateKind: directCriteria.aggregateKind,
                aggregateRange,
                criteriaPairs: resolvedPairs,
                criteriaLayoutCache: nativeDirectCriteriaPredicateLayoutCache,
                criteriaLayoutCacheKey: criteriaVersionKey,
                shouldUseSharedCriteriaCache: () => directCriteriaSharing.directCriteriaShareCount(criteriaVersionKey) > 1,
            });
            if (nativePredicateAggregateResult !== undefined) {
                return applyCachedAggregateResult(nativePredicateAggregateResult);
            }
            matches = args.criterionCache.getOrBuildMatchingRows({
                criteriaPairs: resolvedPairs,
            });
            if (!('tag' in matches)) {
                rememberDirectCriteriaMatch(criteriaVersionKey, matches);
            }
        }
        if ('tag' in matches) {
            return matches;
        }
        if (directCriteria.aggregateKind === 'count') {
            return applyCachedAggregateResult(directNumberResult(matches.length));
        }
        if (!aggregateRange) {
            return undefined;
        }
        const concreteAggregateCacheKey = aggregateCacheKey;
        if (concreteAggregateCacheKey === undefined) {
            return undefined;
        }
        if (directCriteria.aggregateKind === 'first') {
            const firstOffset = matches.rows[0];
            const result = firstOffset === undefined
                ? directErrorResult(ErrorCode.NA)
                : args.runtimeColumnStore
                    .getColumnView({
                    sheetName: aggregateRange.sheetName,
                    rowStart: aggregateRange.rowStart,
                    rowEnd: aggregateRange.rowEnd,
                    col: aggregateRange.col,
                })
                    .readCellValueAt(firstOffset);
            return applyDirectCriteriaResultTransforms(readCellValueByIndex, formula, rememberDirectCriteriaResult(directCriteriaAggregateCache, concreteAggregateCacheKey, result));
        }
        const nativeAggregateResult = tryEvaluateNativeDirectCriteriaMatchedAggregate({
            state: args.state,
            runtimeColumnStore: args.runtimeColumnStore,
        }, {
            aggregateKind: directCriteria.aggregateKind,
            aggregateRange,
            matches,
        });
        if (nativeAggregateResult !== undefined) {
            return applyDirectCriteriaResultTransforms(readCellValueByIndex, formula, rememberDirectCriteriaResult(directCriteriaAggregateCache, concreteAggregateCacheKey, nativeAggregateResult));
        }
        const aggregateView = args.runtimeColumnStore.getColumnView({
            sheetName: aggregateRange.sheetName,
            rowStart: aggregateRange.rowStart,
            rowEnd: aggregateRange.rowEnd,
            col: aggregateRange.col,
        });
        const matchedAggregateError = firstMatchedAggregateError(aggregateView, matches.rows, matches.length);
        if (matchedAggregateError) {
            return applyDirectCriteriaResultTransforms(readCellValueByIndex, formula, rememberDirectCriteriaResult(directCriteriaAggregateCache, concreteAggregateCacheKey, matchedAggregateError));
        }
        if (directCriteria.aggregateKind === 'sum') {
            let sum = 0;
            for (let index = 0; index < matches.length; index += 1) {
                sum += numericLikeValueInView(aggregateView, matches.rows[index]) ?? 0;
            }
            return applyDirectCriteriaResultTransforms(readCellValueByIndex, formula, rememberDirectCriteriaResult(directCriteriaAggregateCache, concreteAggregateCacheKey, directNumberResult(sum)));
        }
        if (directCriteria.aggregateKind === 'average') {
            let count = 0;
            let sum = 0;
            for (let index = 0; index < matches.length; index += 1) {
                const numeric = numericLikeValueInView(aggregateView, matches.rows[index]);
                if (numeric === undefined) {
                    continue;
                }
                count += 1;
                sum += numeric;
            }
            return applyDirectCriteriaResultTransforms(readCellValueByIndex, formula, rememberDirectCriteriaResult(directCriteriaAggregateCache, concreteAggregateCacheKey, count === 0 ? directErrorResult(ErrorCode.Div0) : directNumberResult(sum / count)));
        }
        if (directCriteria.aggregateKind === 'min') {
            let minimum = Number.POSITIVE_INFINITY;
            for (let index = 0; index < matches.length; index += 1) {
                const numeric = strictNumericAggregateCandidateInView(aggregateView, matches.rows[index]);
                if (numeric === undefined) {
                    continue;
                }
                minimum = Math.min(minimum, numeric);
            }
            return applyDirectCriteriaResultTransforms(readCellValueByIndex, formula, rememberDirectCriteriaResult(directCriteriaAggregateCache, concreteAggregateCacheKey, directNumberResult(minimum === Number.POSITIVE_INFINITY ? 0 : minimum)));
        }
        let maximum = Number.NEGATIVE_INFINITY;
        for (let index = 0; index < matches.length; index += 1) {
            const numeric = strictNumericAggregateCandidateInView(aggregateView, matches.rows[index]);
            if (numeric === undefined) {
                continue;
            }
            maximum = Math.max(maximum, numeric);
        }
        return applyDirectCriteriaResultTransforms(readCellValueByIndex, formula, rememberDirectCriteriaResult(directCriteriaAggregateCache, concreteAggregateCacheKey, directNumberResult(maximum === Number.NEGATIVE_INFINITY ? 0 : maximum)));
    };
    const firstMatchedAggregateError = (view, rows, length) => {
        for (let index = 0; index < length; index += 1) {
            const row = rows[index];
            if (view.readTagAt(row) === ValueTag.Error) {
                return directErrorResult(view.readErrorAt(row));
            }
        }
        return undefined;
    };
    const resolveSpillReferenceNow = (currentSheetName, sheetName, address) => {
        const targetSheetName = sheetName ?? currentSheetName;
        const spill = args.state.workbook.getSpill(targetSheetName, address);
        if (!spill) {
            return undefined;
        }
        const owner = parseCellAddress(address, targetSheetName);
        return {
            kind: 'RangeRef',
            refKind: 'cells',
            sheetName: targetSheetName,
            start: owner.text,
            end: formatAddress(owner.row + spill.rows - 1, owner.col + spill.cols - 1),
        };
    };
    const evaluateCellWithReferenceReplacements = (sheetName, address, replacements, visiting) => {
        const replacementKey = referenceReplacementKey(sheetName, address);
        const replacement = replacements.get(replacementKey);
        if (replacement) {
            return evaluateCellWithReferenceReplacements(replacement.sheetName, replacement.address, replacements, visiting);
        }
        const visitKey = referenceReplacementKey(sheetName, address);
        if (visiting.has(visitKey)) {
            return errorValue(ErrorCode.Cycle);
        }
        if (!args.state.workbook.getSheet(sheetName)) {
            return errorValue(ErrorCode.Ref);
        }
        const cellIndex = args.state.workbook.getCellIndex(sheetName, address);
        if (cellIndex === undefined) {
            return emptyValue();
        }
        const formula = args.state.formulas.get(cellIndex);
        if (!formula) {
            return readCellValueByIndex(cellIndex);
        }
        visiting.add(visitKey);
        const { isRowHidden, isRowFiltered } = createRowVisibilityResolvers(args.state.workbook);
        const evaluationContext = {
            sheetName,
            workbookName: args.state.workbook.workbookName,
            currentAddress: address,
            dateSystem: workbookDateSystem(),
            resolveCell: (targetSheetName, targetAddress) => evaluateCellWithReferenceReplacements(targetSheetName, targetAddress, replacements, visiting),
            resolveRange: (targetSheetName, start, end, refKind) => readRangeValues(targetSheetName, start, end, refKind, replacements, visiting),
            resolveName: (name, scopeSheetName) => {
                const definedName = args.state.workbook.getDefinedName(name, scopeSheetName ?? sheetName);
                if (!definedName) {
                    return errorValue(ErrorCode.Name);
                }
                return definedNameValueToCellValue(definedName.value, args.state.strings);
            },
            resolveNameReference: (name, scopeSheetName) => {
                const definedName = args.state.workbook.getDefinedName(name, scopeSheetName ?? sheetName);
                return definedName ? definedNameValueToReferenceOperand(definedName.value) : undefined;
            },
            resolveFormula: (targetSheetName, targetAddress) => {
                const targetCellIndex = args.state.workbook.getCellIndex(targetSheetName, targetAddress);
                return targetCellIndex === undefined ? undefined : args.state.formulas.get(targetCellIndex)?.source;
            },
            resolvePivotData: ({ dataField, sheetName: pivotSheetName, address: pivotAddress, filters, }) => args.resolvePivotData(pivotSheetName, pivotAddress, dataField, filters),
            resolveMultipleOperations: (nested) => resolveMultipleOperationsNow(nested),
            listSheetNames: () => [...args.state.workbook.sheetsByName.values()].toSorted((left, right) => left.order - right.order).map((sheet) => sheet.name),
            isRowHidden,
            isRowFiltered,
            checkEvaluationBudget: (stepCost) => args.checkEvaluationBudget(stepCost),
        };
        const compiled = getRuntimeFormulaStructuralCompiled(formula) ?? formula.compiled;
        const jsPlan = compiled.jsPlan.length > 0
            ? compiled.jsPlan
            : lowerToPlan(compiled.astMatchesSource === false ? parseFormula(compiled.source) : compiled.optimizedAst);
        const result = compiled.producesSpill
            ? evaluatePlanResult(jsPlan, evaluationContext)
            : evaluatePlanScalarResult(jsPlan, evaluationContext);
        visiting.delete(visitKey);
        return isArrayValue(result) ? (result.values[0] ?? emptyValue()) : result;
    };
    const resolveMultipleOperationsNow = (request) => {
        const replacements = new Map();
        replacements.set(referenceReplacementKey(request.rowCellSheetName, request.rowCellAddress), {
            sheetName: request.rowReplacementSheetName,
            address: request.rowReplacementAddress,
        });
        if (request.columnCellSheetName &&
            request.columnCellAddress &&
            request.columnReplacementSheetName &&
            request.columnReplacementAddress) {
            replacements.set(referenceReplacementKey(request.columnCellSheetName, request.columnCellAddress), {
                sheetName: request.columnReplacementSheetName,
                address: request.columnReplacementAddress,
            });
        }
        return evaluateCellWithReferenceReplacements(request.formulaSheetName, request.formulaAddress, replacements, new Set());
    };
    const storeFormulaResult = (cellIndex, formula, result) => {
        const beforeValue = args.state.workbook.cellStore.getValue(cellIndex, (id) => (id === 0 ? '' : args.state.strings.get(id)));
        const materialization = isArrayValue(result)
            ? formula.compiled.producesSpill
                ? args.materializeSpill(cellIndex, result)
                : {
                    changedCellIndices: args.clearOwnedSpill(cellIndex),
                    ownerValue: scalarFromEvaluationResult(result),
                }
            : formula.compiled.producesSpill
                ? {
                    changedCellIndices: args.clearOwnedSpill(cellIndex),
                    ownerValue: result,
                }
                : {
                    changedCellIndices: emptyChangedCellIndices,
                    ownerValue: result,
                };
        args.state.workbook.cellStore.flags[cellIndex] =
            (args.state.workbook.cellStore.flags[cellIndex] ?? 0) & ~(64 /* CellFlags.SpillChild */ | 128 /* CellFlags.PivotOutput */);
        const ownerValue = roundFormulaResultForPrecisionAsDisplayed(args.state, cellIndex, materialization.ownerValue);
        args.state.workbook.cellStore.setValue(cellIndex, ownerValue, ownerValue.tag === ValueTag.String ? args.state.strings.intern(ownerValue.value) : 0);
        if (!cellValuesEqual(beforeValue, ownerValue)) {
            args.state.workbook.notifyCellValueWritten(cellIndex);
        }
        for (let index = 0; index < materialization.changedCellIndices.length; index += 1) {
            args.state.workbook.notifyCellValueWritten(materialization.changedCellIndices[index]);
        }
        return materialization.changedCellIndices;
    };
    const storeDirectScalarResult = (cellIndex, result) => {
        const cellStore = args.state.workbook.cellStore;
        const roundedResult = roundFormulaResultForPrecisionAsDisplayed(args.state, cellIndex, result);
        const beforeTag = cellStore.tags[cellIndex];
        const beforeNumber = cellStore.numbers[cellIndex] ?? 0;
        const beforeStringId = cellStore.stringIds[cellIndex] ?? 0;
        const beforeError = cellStore.errors[cellIndex] ?? ErrorCode.None;
        const nextStringId = roundedResult.tag === ValueTag.String ? args.state.strings.intern(roundedResult.value) : 0;
        const changed = beforeTag !== roundedResult.tag ||
            (roundedResult.tag === ValueTag.Number && !Object.is(beforeNumber, roundedResult.value)) ||
            (roundedResult.tag === ValueTag.Boolean && beforeNumber !== (roundedResult.value ? 1 : 0)) ||
            (roundedResult.tag === ValueTag.String && beforeStringId !== nextStringId) ||
            (roundedResult.tag === ValueTag.Error && beforeError !== roundedResult.code);
        args.state.workbook.cellStore.flags[cellIndex] =
            (args.state.workbook.cellStore.flags[cellIndex] ?? 0) & ~(64 /* CellFlags.SpillChild */ | 128 /* CellFlags.PivotOutput */);
        args.state.workbook.cellStore.setValue(cellIndex, roundedResult, nextStringId);
        if (changed) {
            args.state.workbook.notifyCellValueWritten(cellIndex);
        }
        return emptyChangedCellIndices;
    };
    const evaluateDirectLookupFormulaNow = (cellIndex) => {
        const formula = args.state.formulas.get(cellIndex);
        if (!formula) {
            return undefined;
        }
        const directResult = tryEvaluateDirectVectorLookup(directVectorLookupContext, formula) ??
            tryEvaluateDirectScalar(formula, readCellValueByIndex, workbookDateSystem()) ??
            tryEvaluateDirectAggregate({
                formula,
                workbook: args.state.workbook,
                counters: args.state.counters,
                aggregateCache: args.aggregateCache,
                readCellValueByIndex,
            }) ??
            tryEvaluateDirectCriteriaAggregate(formula, cellIndex) ??
            (formula.inlineScalarFastPlanKind !== undefined ? tryEvaluateFormulaLeafInlineScalar({ state: args.state, formula }) : undefined);
        return directResult === undefined
            ? undefined
            : formula.compiled.producesSpill
                ? storeFormulaResult(cellIndex, formula, directResult)
                : storeDirectScalarResult(cellIndex, directResult);
    };
    const evaluateUnsupportedFormulaNow = (cellIndex) => {
        const formula = args.state.formulas.get(cellIndex);
        const sheetName = args.state.workbook.getSheetNameById(args.state.workbook.cellStore.sheetIds[cellIndex]);
        if (!formula || !sheetName) {
            return [];
        }
        const directResult = tryEvaluateDirectVectorLookup(directVectorLookupContext, formula) ??
            tryEvaluateDirectScalar(formula, readCellValueByIndex, workbookDateSystem()) ??
            tryEvaluateDirectAggregate({
                formula,
                workbook: args.state.workbook,
                counters: args.state.counters,
                aggregateCache: args.aggregateCache,
                readCellValueByIndex,
            }) ??
            tryEvaluateDirectCriteriaAggregate(formula, cellIndex) ??
            (formula.inlineScalarFastPlanKind !== undefined ? tryEvaluateFormulaLeafInlineScalar({ state: args.state, formula }) : undefined);
        if (directResult !== undefined) {
            return storeFormulaResult(cellIndex, formula, directResult);
        }
        const { isRowHidden, isRowFiltered } = createRowVisibilityResolvers(args.state.workbook);
        const evaluationContext = {
            sheetName,
            workbookName: args.state.workbook.workbookName,
            currentAddress: args.state.workbook.getAddress(cellIndex),
            dateSystem: workbookDateSystem(),
            resolveCell: (targetSheetName, address) => readCellValue(targetSheetName, address),
            resolveRange: (targetSheetName, start, end, refKind) => readRangeValues(targetSheetName, start, end, refKind),
            resolveName: (name, scopeSheetName) => {
                const definedName = args.state.workbook.getDefinedName(name, scopeSheetName ?? sheetName);
                if (!definedName) {
                    return errorValue(ErrorCode.Name);
                }
                return definedNameValueToCellValue(definedName.value, args.state.strings);
            },
            resolveNameReference: (name, scopeSheetName) => {
                const definedName = args.state.workbook.getDefinedName(name, scopeSheetName ?? sheetName);
                return definedName ? definedNameValueToReferenceOperand(definedName.value) : undefined;
            },
            resolveFormula: (targetSheetName, address) => {
                const targetCellIndex = args.state.workbook.getCellIndex(targetSheetName, address);
                return targetCellIndex === undefined ? undefined : args.state.formulas.get(targetCellIndex)?.source;
            },
            resolvePivotData: ({ dataField, sheetName: pivotSheetName, address, filters, }) => args.resolvePivotData(pivotSheetName, address, dataField, filters),
            resolveMultipleOperations: (request) => resolveMultipleOperationsNow(request),
            listSheetNames: () => [...args.state.workbook.sheetsByName.values()].toSorted((left, right) => left.order - right.order).map((sheet) => sheet.name),
            isRowHidden,
            isRowFiltered,
            checkEvaluationBudget: (stepCost) => args.checkEvaluationBudget(stepCost),
            resolveExactVectorMatch: (request) => {
                if (request.startRow === undefined ||
                    request.endRow === undefined ||
                    request.startCol === undefined ||
                    request.endCol === undefined) {
                    return args.exactLookup.findVectorMatch(request);
                }
                return resolveExactVectorMatch(formula, request);
            },
            resolveApproximateVectorMatch: (request) => {
                if (request.startRow === undefined ||
                    request.endRow === undefined ||
                    request.startCol === undefined ||
                    request.endCol === undefined) {
                    return args.sortedLookup.findVectorMatch(request);
                }
                return resolveApproximateVectorMatch(formula, request);
            },
            resolveLookupBuiltin,
        };
        const jsPlan = formula.compiled.jsPlan.length > 0 ? formula.compiled.jsPlan : lowerToPlan(formula.compiled.ast);
        const result = formula.compiled.producesSpill
            ? evaluatePlanResult(jsPlan, evaluationContext)
            : evaluatePlanScalarResult(jsPlan, evaluationContext);
        return storeFormulaResult(cellIndex, formula, result);
    };
    const effectWithEngineError = (tryFn, message) => Effect.try({
        try: tryFn,
        catch: (cause) => new EngineFormulaEvaluationError({
            message: evaluationErrorMessage(message, cause),
            cause,
        }),
    });
    return {
        evaluateDirectLookupFormulaNow: evaluateDirectLookupFormulaNow,
        evaluateDirectLookupFormula(cellIndex) {
            return effectWithEngineError(() => evaluateDirectLookupFormulaNow(cellIndex), `Failed to evaluate direct lookup formula ${cellIndex}`);
        },
        evaluateUnsupportedFormula(cellIndex) {
            return effectWithEngineError(() => evaluateUnsupportedFormulaNow(cellIndex), `Failed to evaluate formula ${cellIndex}`);
        },
        evaluateUnsupportedFormulaNow,
        resolveStructuredReference(tableName, columnName, options) {
            return effectWithEngineError(() => resolveStructuredReferenceNow(args.state.workbook, tableName, columnName, options), `Failed to resolve structured reference ${tableName}[${columnName}]`);
        },
        resolveSpillReference(currentSheetName, sheetName, address) {
            return effectWithEngineError(() => resolveSpillReferenceNow(currentSheetName, sheetName, address), `Failed to resolve spill reference ${address}#`);
        },
        resolveMultipleOperations(request) {
            return effectWithEngineError(() => resolveMultipleOperationsNow(request), 'Failed to resolve MULTIPLE.OPERATIONS');
        },
    };
}
//# sourceMappingURL=formula-evaluation-service.js.map