import { LWWRegister } from "../../crdt/LWW.js";
import QueryError from "../../errors/QueryError.js";
import NodeAdd from "../../types/ops/NodeAdd.js";
import EdgeAdd from "../../types/ops/EdgeAdd.js";
import EdgeRemove from "../../types/ops/EdgeRemove.js";
import NodePropSet from "../../types/ops/NodePropSet.js";
import NodeRemove from "../../types/ops/NodeRemove.js";
import { EventId } from "../../utils/EventId.js";
import { normalizeRawOp } from "../OpNormalizer.js";
export default class CheckpointTailFactReducer {
    _graphName;
    constructor(options) {
        this._graphName = options.graphName;
        Object.freeze(this);
    }
    includesNodeLiveness(entry, nodeId) {
        return entry.patch.ops.some((rawOp) => {
            const op = normalizeRawOp(rawOp);
            return (op instanceof NodeAdd || op instanceof NodeRemove)
                && op.node === nodeId;
        });
    }
    includesProperty(entry, nodeId, propertyKey) {
        return entry.patch.ops.some((rawOp) => {
            const op = normalizeRawOp(rawOp);
            return op instanceof NodePropSet && op.node === nodeId && op.key === propertyKey;
        });
    }
    includesNeighborhood(entry, options) {
        return entry.patch.ops.some((rawOp) => {
            const op = normalizeRawOp(rawOp);
            return isTargetNeighborhoodEdge(op, options);
        });
    }
    reduceNodeLiveness(baseAlive, tailEntries, nodeId) {
        let alive = baseAlive;
        for (const entry of tailEntries) {
            alive = this._reduceNodeLivenessEntry(alive, entry, nodeId);
        }
        return alive;
    }
    reduceProperty(options) {
        const tailRegister = this._tailPropertyRegister(options);
        return tailRegister !== null ? tailRegister.value : options.baseValue;
    }
    assertNeighborhoodTailStable(tailEntries) {
        if (tailEntries.length > 0) {
            throwNoBoundedBasis(this._graphName, 'tail-neighborhood-needs-adjacency-witnesses');
        }
    }
    _reduceNodeLivenessEntry(currentAlive, entry, nodeId) {
        let alive = currentAlive;
        for (const rawOp of entry.patch.ops) {
            alive = this._reduceNodeLivenessOp(alive, normalizeRawOp(rawOp), nodeId);
        }
        return alive;
    }
    _reduceNodeLivenessOp(currentAlive, op, nodeId) {
        if (isTargetNodeAdd(op, nodeId)) {
            return true;
        }
        if (isTargetNodeRemove(op, nodeId)) {
            throwNoBoundedBasis(this._graphName, 'tail-node-remove-needs-raw-liveness-witnesses');
        }
        return currentAlive;
    }
    _tailPropertyRegister(options) {
        let tailRegister = null;
        for (const entry of options.tailEntries) {
            tailRegister = this._addTailPropertyEntryRegister(tailRegister, entry, options);
        }
        return tailRegister;
    }
    _addTailPropertyEntryRegister(current, entry, options) {
        let tailRegister = current;
        for (let opIndex = 0; opIndex < entry.patch.ops.length; opIndex += 1) {
            const rawOp = entry.patch.ops[opIndex];
            if (rawOp !== undefined) {
                tailRegister = this._addTailPropertyOperationRegister({
                    current: tailRegister,
                    entry,
                    nodeId: options.nodeId,
                    op: normalizeRawOp(rawOp),
                    opIndex,
                    propertyKey: options.propertyKey,
                });
            }
        }
        return tailRegister;
    }
    _addTailPropertyOperationRegister(options) {
        if (!isTargetPropertyOp(options.op, options.nodeId, options.propertyKey)) {
            return options.current;
        }
        return LWWRegister.max(options.current, new LWWRegister(new EventId(options.entry.patch.lamport, options.entry.patch.writer, options.entry.sha, options.opIndex), readScalarTailPropertyValue(options.op, this._graphName)));
    }
}
function isTargetPropertyOp(op, nodeId, propertyKey) {
    return op instanceof NodePropSet && op.node === nodeId && op.key === propertyKey;
}
function isTargetNodeAdd(op, nodeId) {
    return op instanceof NodeAdd && op.node === nodeId;
}
function isTargetNodeRemove(op, nodeId) {
    return op instanceof NodeRemove && op.node === nodeId;
}
function isTargetNeighborhoodEdge(op, options) {
    if (!(op instanceof EdgeAdd) && !(op instanceof EdgeRemove)) {
        return false;
    }
    return matchesNeighborhoodLabel(op, options.labels)
        && matchesNeighborhoodDirection(op, options.nodeId, options.direction);
}
function matchesNeighborhoodLabel(op, labels) {
    return labels.length === 0 || labels.includes(op.label);
}
function matchesNeighborhoodDirection(op, nodeId, direction) {
    if (direction === 'out') {
        return op.from === nodeId;
    }
    if (direction === 'in') {
        return op.to === nodeId;
    }
    return op.from === nodeId || op.to === nodeId;
}
function readScalarTailPropertyValue(op, graphName) {
    const { value } = op;
    if (isPrimitiveTailPropertyValue(value) || value instanceof Uint8Array) {
        return value;
    }
    return throwNoBoundedBasis(graphName, 'tail-property-value-needs-parser');
}
function isPrimitiveTailPropertyValue(value) {
    return value === null
        || typeof value === 'string'
        || typeof value === 'number'
        || typeof value === 'boolean';
}
function throwNoBoundedBasis(graphName, reason) {
    throw new QueryError('No bounded checkpoint-tail optic basis is available.', {
        code: 'E_OPTIC_NO_BOUNDED_BASIS',
        context: { graphName, reason },
    });
}
