/**
 * Deterministic force-directed layout for the graph-render tool.
 *
 * Uses d3-force with a seeded LCG so the same input graph always produces
 * the same node positions. Layout fidelity to the live /graph page (which
 * runs vis-network's Barnes-Hut port in the browser) is NOT a goal —
 * d3-force's many-body repulsion plus link spring forces give a coherent
 * force-directed layout, which is what the operator needs to read the
 * subgraph. Pixel-equivalence with /graph is filed as a follow-up task.
 *
 * Convergence: a fixed iteration count rather than a kinetic-energy
 * threshold. The kinetic-energy approach is sensitive to graph topology
 * (sparse graphs converge in 50 iterations, dense ones never stabilise
 * cleanly), so 300 iterations is the simple, deterministic choice. The
 * caller logs whether iteration completed successfully.
 */
import { forceSimulation, forceLink, forceManyBody, forceCenter, forceX, forceY, } from "d3-force";
/**
 * Seeded LCG (linear-congruential generator). Used as d3-force's
 * randomSource so layout is deterministic across runs. Values are the
 * Numerical Recipes constants — fast, adequate for jittering initial
 * positions, not cryptographically interesting.
 */
function seededRandom(seed) {
    let state = seed >>> 0;
    return () => {
        state = (state * 1664525 + 1013904223) >>> 0;
        return state / 0x100000000;
    };
}
const ITERATIONS = 300;
const LINK_DISTANCE = 90;
const CHARGE_STRENGTH = -240;
const CENTER_STRENGTH = 0.05;
const FIT_PADDING = 60;
export function layoutGraph(nodes, edges, opts) {
    const seed = opts.seed ?? 1;
    const rand = seededRandom(seed);
    // Drop edges whose endpoints aren't in the node set — d3-force will
    // throw on a missing endpoint, and the upstream Cypher might return
    // edges whose far side was capped off by the truncation cut.
    const nodeIds = new Set(nodes.map((n) => n.id));
    const validEdges = edges.filter((e) => nodeIds.has(e.from) && nodeIds.has(e.to));
    // Initial positions: jittered around the centre. d3-force will
    // displace them over the iteration count.
    const cx = opts.width / 2;
    const cy = opts.height / 2;
    const simNodes = nodes.map((n) => ({
        id: n.id,
        labels: n.labels,
        x: cx + (rand() - 0.5) * 40,
        y: cy + (rand() - 0.5) * 40,
    }));
    const simLinks = validEdges.map((e) => ({
        source: e.from,
        target: e.to,
        type: e.type,
    }));
    const simulation = forceSimulation(simNodes)
        .randomSource(rand)
        .force("link", forceLink(simLinks)
        .id((d) => d.id)
        .distance(LINK_DISTANCE))
        .force("charge", forceManyBody().strength(CHARGE_STRENGTH))
        .force("center", forceCenter(cx, cy))
        .force("x", forceX(cx).strength(CENTER_STRENGTH))
        .force("y", forceY(cy).strength(CENTER_STRENGTH))
        .stop();
    simulation.tick(ITERATIONS);
    // Fit-to-content pass. The force simulation settles into a cluster that
    // can be much smaller than the canvas (typical for sparse subgraphs of a
    // few nodes), leaving the operator with two tiny dots in the middle of a
    // 1200×900 image. Compute the bounding box of the settled positions and
    // apply a uniform scale + translate so the cluster fills the canvas with
    // a `FIT_PADDING` border. Aspect ratio is preserved. Degenerate cases
    // (single node, or all nodes collapsed onto one point) skip scaling and
    // are centred at (cx, cy).
    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
    for (const n of simNodes) {
        const x = n.x ?? cx;
        const y = n.y ?? cy;
        if (x < minX)
            minX = x;
        if (x > maxX)
            maxX = x;
        if (y < minY)
            minY = y;
        if (y > maxY)
            maxY = y;
    }
    const bboxW = maxX - minX;
    const bboxH = maxY - minY;
    const targetW = opts.width - 2 * FIT_PADDING;
    const targetH = opts.height - 2 * FIT_PADDING;
    // Degenerate when a single node, or all nodes collapsed within one pixel.
    const degenerate = simNodes.length <= 1 || (bboxW < 1 && bboxH < 1);
    const scale = degenerate
        ? 1
        : Math.min(targetW / Math.max(bboxW, 1), targetH / Math.max(bboxH, 1));
    const outNodes = simNodes.map((n) => {
        const x = n.x ?? cx;
        const y = n.y ?? cy;
        if (degenerate)
            return { id: n.id, labels: n.labels, x: cx, y: cy };
        // Centre the scaled bbox on the canvas centre.
        const scaledCx = (minX + bboxW / 2) * scale;
        const scaledCy = (minY + bboxH / 2) * scale;
        return {
            id: n.id,
            labels: n.labels,
            x: x * scale - scaledCx + cx,
            y: y * scale - scaledCy + cy,
        };
    });
    return { nodes: outNodes, edges: validEdges, iterations: ITERATIONS };
}
//# sourceMappingURL=layout.js.map