import { evalConflict, placementBias, W } from './cost';
/** Conflicts below this are treated as resolved (floating-point slack). */
const RESOLVED = 0.5;
/**
 * The slot that minimizes conflict (crossings/overlaps), breaking ties by the
 * tidiness bias. Selection and the caller's acceptance gate share the conflict
 * objective, so a node is never locked at a placement another slot would
 * improve. The baseCost early-out applies only once conflict is already
 * resolved — while a node still conflicts, every slot is considered.
 */
const bestSlot = (node, config) => {
    const neighbors = node.neighbors;
    let index = node.slotIndex;
    let conflict = evalConflict(node, node.slotIndex, neighbors, config);
    let bias = placementBias(node, node.slotIndex);
    for (let s = 0; s < node.slots.length; s++) {
        if (s === node.slotIndex)
            continue;
        // Slots are sorted ascending by baseCost; once conflict is resolved only
        // tidiness remains, so no farther slot can beat the incumbent's bias.
        if (conflict <= RESOLVED && node.slots[s].baseCost - W.stick >= bias)
            break;
        const c = evalConflict(node, s, neighbors, config);
        const b = placementBias(node, s);
        if (c < conflict - 1e-6 || (c <= conflict + 1e-6 && b < bias)) {
            index = s;
            conflict = c;
            bias = b;
        }
    }
    return { index, conflict };
};
export const solve = (nodes, config, bestSnap) => {
    const n = nodes.length;
    if (n === 0)
        return;
    for (const node of nodes)
        node.prevSlotIndex = node.slotIndex;
    const order = nodes.toSorted((a, b) => b.w * b.h - a.w * a.h || a.slots.length - b.slots.length || a.idHash - b.idHash);
    let total = 0;
    for (let i = 0; i < n; i++) {
        const node = nodes[i];
        node.conflict = evalConflict(node, node.slotIndex, node.neighbors, config);
        node.locked = false;
        total += node.conflict;
    }
    let bestE = total;
    for (let i = 0; i < n; i++)
        bestSnap[i] = nodes[i].slotIndex;
    let budget = config.polishBudget;
    while (budget > 0) {
        let worst;
        let worstConflict = RESOLVED;
        for (let i = 0; i < n; i++) {
            const node = order[i];
            if (!node.locked && node.conflict > worstConflict) {
                worstConflict = node.conflict;
                worst = node;
            }
        }
        if (!worst)
            break;
        const result = bestSlot(worst, config);
        if (result.index !== worst.slotIndex && result.conflict < worst.conflict - RESOLVED) {
            total += result.conflict - worst.conflict;
            worst.slotIndex = result.index;
            worst.conflict = result.conflict;
            budget--;
            // Moving `worst` changes the pairwise cost of its neighbors only.
            for (const m of worst.neighbors) {
                const before = m.conflict;
                m.conflict = evalConflict(m, m.slotIndex, m.neighbors, config);
                total += m.conflict - before;
                m.locked = false;
            }
            if (total < bestE) {
                bestE = total;
                for (let i = 0; i < n; i++)
                    bestSnap[i] = nodes[i].slotIndex;
            }
        }
        else {
            worst.locked = true;
        }
    }
    for (let i = 0; i < n; i++) {
        const node = nodes[i];
        node.slotIndex = bestSnap[i];
        const slot = node.slots[node.slotIndex];
        node.tx = node.ax + slot.dx;
        node.ty = node.ay + slot.dy;
    }
};
