/**
 * Structural diff and node-view comparison for visible-state diffing.
 *
 * Covers neighbor list comparison, node-view normalization, node/edge set deltas,
 * and the top-level "has anything changed" check.
 * Pure functions — no side effects, no I/O, no classes.
 *
 * @module domain/services/comparison/diffStructure
 */
import { compareStrings, compareNeighbors, compareEdgeRefs, collectEdges, neighborKey, valueKey, } from "./diffKeys.js";
import { compareNodeViewProperties } from "./diffProperties.js";
// ── Neighbor list comparison ─────────────────────────────────────────────────
/**
 * Builds a map from composite neighbor key to neighbor object.
 */
export function neighborMap(neighbors) {
    return new Map(neighbors.map((neighbor) => [neighborKey(neighbor), neighbor]));
}
/**
 * Computes the added and removed deltas between two neighbor lists.
 */
export function compareNeighborLists(leftNeighbors, rightNeighbors) {
    const leftMap = neighborMap(leftNeighbors);
    const rightMap = neighborMap(rightNeighbors);
    return {
        added: [...rightMap.entries()]
            .filter(([key]) => !leftMap.has(key))
            .map(([, value]) => value)
            .sort(compareNeighbors),
        removed: [...leftMap.entries()]
            .filter(([key]) => !rightMap.has(key))
            .map(([, value]) => value)
            .sort(compareNeighbors),
    };
}
// ── Change detection ─────────────────────────────────────────────────────────
/**
 * Returns true if any of the provided arrays has at least one entry.
 */
export function hasAnyEntries(groups) {
    return groups.some((group) => group.length > 0);
}
export const EMPTY_NODE_VIEW = {
    exists: false,
    nodeId: null,
    props: {},
    outgoing: [],
    incoming: [],
    content: null,
};
/**
 * Extracts fields from a non-null node view with defaults applied.
 */
export function extractNodeView(view) {
    return {
        exists: true,
        nodeId: view.nodeId,
        props: view.props ?? {},
        outgoing: view.outgoing ?? [],
        incoming: view.incoming ?? [],
        content: view.content ?? null,
    };
}
/**
 * Normalizes a nullable node view into a consistent shape with defaults.
 */
export function normalizeNodeView(view) {
    if (view === null || view === undefined) {
        return { ...EMPTY_NODE_VIEW };
    }
    return extractNodeView(view);
}
/**
 * Determines whether a node view comparison has any structural changes.
 */
export function hasNodeViewChanges({ propertyDelta, outgoingDelta, incomingDelta, contentChanged, leftExists, rightExists, }) {
    return (hasAnyEntries([
        propertyDelta.added,
        propertyDelta.removed,
        propertyDelta.changed,
        outgoingDelta.added,
        outgoingDelta.removed,
        incomingDelta.added,
        incomingDelta.removed,
    ]) ||
        contentChanged ||
        leftExists !== rightExists);
}
/**
 * Computes all per-view deltas from two normalized node views.
 */
function diffNormalizedViews(leftView, rightView) {
    return {
        targetId: leftView.nodeId ?? rightView.nodeId ?? null,
        propertyDelta: compareNodeViewProperties(leftView.props, rightView.props),
        outgoingDelta: compareNeighborLists(leftView.outgoing, rightView.outgoing),
        incomingDelta: compareNeighborLists(leftView.incoming, rightView.incoming),
        contentChanged: valueKey(leftView.content) !== valueKey(rightView.content),
        leftExists: leftView.exists,
        rightExists: rightView.exists,
    };
}
/**
 * Compares two nullable node views and returns a detailed diff result.
 */
export function compareNodeViews(left, right) {
    const d = diffNormalizedViews(normalizeNodeView(left), normalizeNodeView(right));
    const changed = hasNodeViewChanges(d);
    return {
        targetId: d.targetId,
        leftExists: d.leftExists,
        rightExists: d.rightExists,
        changed,
        left,
        right,
        propertyDelta: d.propertyDelta,
        outgoingDelta: d.outgoingDelta,
        incomingDelta: d.incomingDelta,
        contentChanged: d.contentChanged,
    };
}
// ── Node / edge set deltas ───────────────────────────────────────────────────
/**
 * Computes the set-difference delta of node IDs between two readers.
 */
export function buildNodeDelta(leftReader, rightReader) {
    const leftNodes = new Set(leftReader.getNodes());
    const rightNodes = new Set(rightReader.getNodes());
    return {
        added: [...rightNodes].filter((nodeId) => !leftNodes.has(nodeId)).sort(compareStrings),
        removed: [...leftNodes].filter((nodeId) => !rightNodes.has(nodeId)).sort(compareStrings),
    };
}
/**
 * Computes the set-difference delta of edges between two readers.
 */
export function buildEdgeDelta(leftReader, rightReader) {
    const leftEdges = collectEdges(leftReader);
    const rightEdges = collectEdges(rightReader);
    return {
        added: [...rightEdges.entries()]
            .filter(([key]) => !leftEdges.has(key))
            .map(([, value]) => value)
            .sort(compareEdgeRefs),
        removed: [...leftEdges.entries()]
            .filter(([key]) => !rightEdges.has(key))
            .map(([, value]) => value)
            .sort(compareEdgeRefs),
    };
}
/**
 * Returns true if any delta array has entries, indicating visible state changes.
 */
export function hasVisibleStateChanges({ nodeDelta, edgeDelta, nodePropertyDelta, edgePropertyDelta, }) {
    return hasAnyEntries([
        nodeDelta.added,
        nodeDelta.removed,
        edgeDelta.added,
        edgeDelta.removed,
        nodePropertyDelta.added,
        nodePropertyDelta.removed,
        nodePropertyDelta.changed,
        edgePropertyDelta.added,
        edgePropertyDelta.removed,
        edgePropertyDelta.changed,
    ]);
}
/**
 * Normalizes a potentially null or empty target ID to a trimmed string or null.
 */
export function normalizeTargetId(targetId) {
    return typeof targetId === 'string' && targetId.trim().length > 0
        ? targetId.trim()
        : null;
}
/**
 * Builds a node-level comparison for a specific target, or undefined if no target.
 */
export function buildTargetComparison(leftReader, rightReader, targetId) {
    if (typeof targetId !== 'string' || targetId.length === 0) {
        return undefined;
    }
    return compareNodeViews(leftReader.inspectNode(targetId), rightReader.inspectNode(targetId));
}
