function normalizedDefinedName(name) {
    return name.trim().toUpperCase();
}
function pivotEntityKey(sheetName, address) {
    return `pivot:${sheetName}!${address}`;
}
function chartEntityKey(id) {
    return `chart:${id.trim().toUpperCase()}`;
}
function imageEntityKey(id) {
    return `image:${id.trim().toUpperCase()}`;
}
function shapeEntityKey(id) {
    return `shape:${id.trim().toUpperCase()}`;
}
export function createReplicaState(replicaId) {
    return {
        replicaId,
        clock: { counter: 0 },
        appliedBatchIds: new Set(),
    };
}
export function hydrateReplicaState(state, snapshot) {
    state.replicaId = snapshot.replicaId;
    state.clock.counter = snapshot.counter;
    state.appliedBatchIds.clear();
    snapshot.appliedBatchIds.forEach((id) => state.appliedBatchIds.add(id));
}
export function exportReplicaSnapshot(state, limit = 2048) {
    const appliedBatchIds = [...state.appliedBatchIds].toSorted();
    const trimmed = appliedBatchIds.slice(Math.max(0, appliedBatchIds.length - limit));
    return {
        replicaId: state.replicaId,
        counter: state.clock.counter,
        appliedBatchIds: trimmed,
    };
}
export function importReplicaSnapshot(snapshot) {
    const state = createReplicaState(snapshot.replicaId);
    hydrateReplicaState(state, snapshot);
    return state;
}
function nextClock(state) {
    state.clock.counter += 1;
    return { counter: state.clock.counter };
}
export function createBatch(state, ops) {
    const clock = nextClock(state);
    const batch = {
        id: `${state.replicaId}:${clock.counter}`,
        replicaId: state.replicaId,
        clock,
        ops,
    };
    markBatchApplied(state, batch);
    return batch;
}
export function createLocalOpOrder(state, opIndex = 0) {
    state.clock.counter += 1;
    const counter = state.clock.counter;
    const batchId = `${state.replicaId}:${counter}`;
    state.appliedBatchIds.add(batchId);
    return {
        counter,
        replicaId: state.replicaId,
        batchId,
        opIndex,
    };
}
export function markBatchApplied(state, batch) {
    state.appliedBatchIds.add(batch.id);
    state.clock.counter = Math.max(state.clock.counter, batch.clock.counter);
}
export function shouldApplyBatch(state, batch) {
    return !state.appliedBatchIds.has(batch.id);
}
function compareBatches(left, right) {
    return left.clock.counter - right.clock.counter || left.replicaId.localeCompare(right.replicaId) || left.id.localeCompare(right.id);
}
export function batchOpOrder(batch, opIndex) {
    return {
        counter: batch.clock.counter,
        replicaId: batch.replicaId,
        batchId: batch.id,
        opIndex,
    };
}
export function compareOpOrder(left, right) {
    return (left.counter - right.counter ||
        left.replicaId.localeCompare(right.replicaId) ||
        left.batchId.localeCompare(right.batchId) ||
        left.opIndex - right.opIndex);
}
function assertNever(value) {
    throw new Error(`Unhandled engine op kind: ${JSON.stringify(value)}`);
}
function entityKeyForOp(op) {
    switch (op.kind) {
        case 'upsertWorkbook':
            return 'workbook';
        case 'setWorkbookMetadata':
            return `workbook-meta:${op.key}`;
        case 'setCalculationSettings':
            return 'workbook-calc';
        case 'setVolatileContext':
            return 'workbook-volatile';
        case 'upsertSheet':
        case 'deleteSheet':
            return `sheet:${op.name}`;
        case 'renameSheet':
            return `sheet:${op.oldName}`;
        case 'insertRows':
        case 'deleteRows':
        case 'moveRows':
            return `row-structure:${op.sheetName}`;
        case 'insertColumns':
        case 'deleteColumns':
        case 'moveColumns':
            return `column-structure:${op.sheetName}`;
        case 'updateRowMetadata':
            return `row-meta:${op.sheetName}:${op.start}:${op.count}`;
        case 'updateColumnMetadata':
            return `column-meta:${op.sheetName}:${op.start}:${op.count}`;
        case 'setFreezePane':
        case 'clearFreezePane':
            return `freeze:${op.sheetName}`;
        case 'mergeCells':
        case 'unmergeCells':
            return `merge:${op.range.sheetName}:${op.range.startAddress}:${op.range.endAddress}`;
        case 'setSheetProtection':
        case 'clearSheetProtection':
            return `sheet-protection:${op.kind === 'setSheetProtection' ? op.protection.sheetName : op.sheetName}`;
        case 'setFilter':
        case 'clearFilter':
            return `filter:${op.sheetName}:${op.range.startAddress}:${op.range.endAddress}`;
        case 'setSort':
        case 'clearSort':
            return `sort:${op.sheetName}:${op.range.startAddress}:${op.range.endAddress}`;
        case 'setDataValidation':
            return `validation:${op.validation.range.sheetName}:${op.validation.range.startAddress}:${op.validation.range.endAddress}`;
        case 'clearDataValidation':
            return `validation:${op.sheetName}:${op.range.startAddress}:${op.range.endAddress}`;
        case 'upsertConditionalFormat':
            return `conditional-format:${op.format.id}`;
        case 'deleteConditionalFormat':
            return `conditional-format:${op.id}`;
        case 'setConditionalFormatArtifacts':
        case 'clearConditionalFormatArtifacts':
            return `conditional-format-artifacts:${op.sheetName}`;
        case 'upsertRangeProtection':
            return `range-protection:${op.protection.id}`;
        case 'deleteRangeProtection':
            return `range-protection:${op.id}`;
        case 'upsertCommentThread':
            return `comment:${op.thread.sheetName}!${op.thread.address}`;
        case 'deleteCommentThread':
            return `comment:${op.sheetName}!${op.address}`;
        case 'upsertNote':
            return `note:${op.note.sheetName}!${op.note.address}`;
        case 'deleteNote':
            return `note:${op.sheetName}!${op.address}`;
        case 'upsertHyperlink':
            return `hyperlink:${op.hyperlink.sheetName}!${op.hyperlink.address}`;
        case 'deleteHyperlink':
            return `hyperlink:${op.sheetName}!${op.address}`;
        case 'setCellValue':
        case 'setCellFormula':
        case 'clearCell':
            return `cell:${op.sheetName}!${op.address}`;
        case 'setCellFormat':
            return `format:${op.sheetName}!${op.address}`;
        case 'upsertCellStyle':
            return `style:${op.style.id}`;
        case 'upsertCellNumberFormat':
            return `number-format:${op.format.id}`;
        case 'setStyleRange':
            return `style-range:${op.range.sheetName}:${op.range.startAddress}:${op.range.endAddress}`;
        case 'setFormatRange':
            return `format-range:${op.range.sheetName}:${op.range.startAddress}:${op.range.endAddress}`;
        case 'upsertDefinedName':
        case 'deleteDefinedName':
            return `defined-name:${normalizedDefinedName(op.name)}`;
        case 'upsertTable':
            return `table:${normalizedDefinedName(op.table.name)}`;
        case 'deleteTable':
            return `table:${normalizedDefinedName(op.name)}`;
        case 'upsertSpillRange':
        case 'deleteSpillRange':
            return `spill:${op.sheetName}!${op.address}`;
        case 'upsertPivotTable':
        case 'deletePivotTable':
            return pivotEntityKey(op.sheetName, op.address);
        case 'upsertChart':
            return chartEntityKey(op.chart.id);
        case 'deleteChart':
            return chartEntityKey(op.id);
        case 'upsertImage':
            return imageEntityKey(op.image.id);
        case 'deleteImage':
            return imageEntityKey(op.id);
        case 'upsertShape':
            return shapeEntityKey(op.shape.id);
        case 'deleteShape':
            return shapeEntityKey(op.id);
    }
    return assertNever(op);
}
function sheetDeleteBarrierForOp(op, latestSheetDeletes) {
    switch (op.kind) {
        case 'upsertWorkbook':
        case 'setWorkbookMetadata':
        case 'setCalculationSettings':
        case 'setVolatileContext':
        case 'deleteSheet':
        case 'upsertDefinedName':
        case 'deleteDefinedName':
        case 'upsertTable':
        case 'deleteTable':
            return undefined;
        case 'upsertSheet':
            return latestSheetDeletes.get(op.name);
        case 'renameSheet':
            return latestSheetDeletes.get(op.oldName);
        case 'insertRows':
        case 'deleteRows':
        case 'moveRows':
        case 'insertColumns':
        case 'deleteColumns':
        case 'moveColumns':
            return latestSheetDeletes.get(op.sheetName);
        case 'updateRowMetadata':
        case 'updateColumnMetadata':
        case 'setFreezePane':
        case 'clearFreezePane':
        case 'clearSheetProtection':
        case 'setFilter':
        case 'clearFilter':
        case 'setSort':
        case 'clearSort':
        case 'clearDataValidation':
        case 'deleteConditionalFormat':
        case 'setConditionalFormatArtifacts':
        case 'clearConditionalFormatArtifacts':
        case 'deleteRangeProtection':
        case 'deleteCommentThread':
        case 'deleteNote':
        case 'deleteHyperlink':
        case 'setCellValue':
        case 'setCellFormula':
        case 'setCellFormat':
        case 'clearCell':
        case 'upsertSpillRange':
        case 'deleteSpillRange':
        case 'deletePivotTable':
            return latestSheetDeletes.get(op.sheetName);
        case 'mergeCells':
        case 'unmergeCells':
        case 'setStyleRange':
        case 'setFormatRange':
            return latestSheetDeletes.get(op.range.sheetName);
        case 'upsertCellNumberFormat':
        case 'upsertCellStyle':
            return undefined;
        case 'setDataValidation':
            return latestSheetDeletes.get(op.validation.range.sheetName);
        case 'setSheetProtection':
            return latestSheetDeletes.get(op.protection.sheetName);
        case 'upsertConditionalFormat':
            return latestSheetDeletes.get(op.format.range.sheetName);
        case 'upsertRangeProtection':
            return latestSheetDeletes.get(op.protection.range.sheetName);
        case 'upsertCommentThread':
            return latestSheetDeletes.get(op.thread.sheetName);
        case 'upsertNote':
            return latestSheetDeletes.get(op.note.sheetName);
        case 'upsertHyperlink':
            return latestSheetDeletes.get(op.hyperlink.sheetName);
        case 'upsertPivotTable':
            return latestSheetDeletes.get(op.sheetName) ?? latestSheetDeletes.get(op.source.sheetName);
        case 'upsertChart':
            return latestSheetDeletes.get(op.chart.sheetName) ?? latestSheetDeletes.get(op.chart.source.sheetName);
        case 'deleteChart':
            return undefined;
        case 'upsertImage':
            return latestSheetDeletes.get(op.image.sheetName);
        case 'deleteImage':
            return undefined;
        case 'upsertShape':
            return latestSheetDeletes.get(op.shape.sheetName);
        case 'deleteShape':
            return undefined;
    }
    return assertNever(op);
}
export function compactLog(batches) {
    const deduped = new Map();
    batches.forEach((batch) => {
        deduped.set(batch.id, batch);
    });
    const ordered = [...deduped.values()].toSorted(compareBatches);
    const latestByEntity = new Map();
    const latestSheetDeletes = new Map();
    ordered.forEach((batch) => {
        batch.ops.forEach((op, opIndex) => {
            const order = batchOpOrder(batch, opIndex);
            latestByEntity.set(entityKeyForOp(op), order);
            if (op.kind === 'deleteSheet') {
                latestSheetDeletes.set(op.name, order);
            }
        });
    });
    return ordered
        .map((batch) => {
        const ops = batch.ops.filter((op, opIndex) => {
            const order = batchOpOrder(batch, opIndex);
            const sheetDeleteBarrier = sheetDeleteBarrierForOp(op, latestSheetDeletes);
            if (sheetDeleteBarrier && compareOpOrder(order, sheetDeleteBarrier) <= 0) {
                return false;
            }
            const latestOrder = latestByEntity.get(entityKeyForOp(op));
            return latestOrder !== undefined && compareOpOrder(order, latestOrder) === 0;
        });
        return {
            id: batch.id,
            replicaId: batch.replicaId,
            clock: batch.clock,
            ops,
        };
    })
        .filter((batch) => batch.ops.length > 0);
}
export function mergeBatches(batches) {
    const deduped = new Map();
    batches.forEach((batch) => {
        deduped.set(batch.id, batch);
    });
    return [...deduped.values()].toSorted(compareBatches);
}
//# sourceMappingURL=replica-state.js.map