/**
 * 2-way Fiduccia–Mattheyses cut refinement (single-constraint, cut objective).
 *
 * Maintains two max-heaps keyed by per-vertex gain (`external_degree − internal_degree`),
 * one per side of the bisection. Each pass:
 *   1. Insert every boundary vertex into the queue for its current side.
 *   2. Repeatedly pop from whichever side is more overweight, move the vertex,
 *      and update neighbours. Track running cut and remember the best snapshot.
 *   3. Stop when both queues drain, or when `LIMIT_NONIMPROVING_MOVES` moves
 *      pass without finding a better cut.
 *   4. Roll back any moves made after the best-cut snapshot.
 *
 * Each vertex is locked after moving once (it can't move back in the same pass).
 * Locking is reset between passes.
 *
 * Mirrors `FM_2WayCutRefine` from libmetis/fm.c.
 *
 * All scratch state (heaps, moved-index, swap history, insertion order) is
 * shared via the `BisectionScratch` passed in by the caller, sized to the
 * coarsest graph's vertex_count. This eliminates per-trial allocation.
 *
 * @param {import('../MetisGraph.js').MetisGraph} graph
 * @param {number} target_weight_for_partition_0
 * @param {number} pass_count
 * @param {function():number} random seeded RNG used to shuffle boundary
 *   insertion order so ties break differently each pass
 * @param {import('./BisectionScratch.js').BisectionScratch} scratch
 */
export function fm_2way(graph, target_weight_for_partition_0, pass_count, random, scratch) {
    const vertex_count = graph.vertex_count;
    const edge_addresses = graph.edge_addresses;
    const adjacency = graph.adjacency;
    const edge_weights = graph.edge_weights;
    const vertex_weights = graph.vertex_weights;
    const vertex_partition = graph.vertex_partition;
    const partition_weights = graph.partition_weights;
    const internal_degree = graph.internal_degree;
    const external_degree = graph.external_degree;
    const boundary_vertices = graph.boundary_vertices;
    const boundary_position_of_vertex = graph.boundary_position_of_vertex;

    if (vertex_count === 0 || graph.boundary_count === 0) {
        return;
    }

    const target_weight_0 = target_weight_for_partition_0;
    const target_weight_1 = graph.total_vertex_weight - target_weight_for_partition_0;

    // METIS heuristic: stop a pass after this many moves with no improvement.
    const limit_nonimproving_moves = Math.min(Math.max(Math.floor(0.01 * vertex_count), 15), 100);

    // METIS uses this for a "close enough to mincut" tolerance when picking
    // the best snapshot — avoids overreacting to small noise in pwgt-balance.
    const avg_vertex_weight = Math.min(
        Math.floor((partition_weights[0] + partition_weights[1]) / 20),
        Math.floor(2 * (partition_weights[0] + partition_weights[1]) / vertex_count)
    );

    const queues = scratch.fm_heaps;
    const moved_pass_index = scratch.fm_moved_pass_index;
    const swap_history = scratch.fm_swap_history;
    const insertion_order = scratch.fm_insertion_order;

    const initial_partition_0_diff = Math.abs(target_weight_0 - partition_weights[0]);

    for (let pass = 0; pass < pass_count; pass++) {
        queues[0].clear();
        queues[1].clear();

        let best_cut = graph.current_cut;
        let best_partition_diff = Math.abs(target_weight_0 - partition_weights[0]);
        let best_cut_swap_index = -1;
        let running_cut = best_cut;
        let initial_cut_this_pass = best_cut;

        let boundary_count = graph.boundary_count;

        // Insert boundary vertices in random order so ties break differently each pass.
        for (let i = 0; i < boundary_count; i++) {
            insertion_order[i] = i;
        }
        // Fisher–Yates shuffle of insertion_order[0..boundary_count)
        for (let i = boundary_count - 1; i > 0; i--) {
            const j = (random() * (i + 1)) | 0;
            if (j !== i) {
                const tmp = insertion_order[i];
                insertion_order[i] = insertion_order[j];
                insertion_order[j] = tmp;
            }
        }
        for (let i = 0; i < boundary_count; i++) {
            const vertex = boundary_vertices[insertion_order[i]];
            const gain = external_degree[vertex] - internal_degree[vertex];
            queues[vertex_partition[vertex]].insert(vertex, gain);
        }

        let swap_count = 0;
        for (; swap_count < vertex_count; swap_count++) {
            // Pick the side that's more overweight (or equivalently, more under-target).
            const from_side = (target_weight_0 - partition_weights[0]) < (target_weight_1 - partition_weights[1]) ? 0 : 1;
            const to_side = 1 - from_side;

            const high_gain_vertex = queues[from_side].pop_max();
            if (high_gain_vertex === -1) {
                break;
            }

            const gain = external_degree[high_gain_vertex] - internal_degree[high_gain_vertex];
            running_cut -= gain;

            partition_weights[from_side] -= vertex_weights[high_gain_vertex];
            partition_weights[to_side] += vertex_weights[high_gain_vertex];

            const new_partition_diff = Math.abs(target_weight_0 - partition_weights[0]);

            const improves_cut_within_balance = (
                running_cut < best_cut &&
                new_partition_diff <= initial_partition_0_diff + avg_vertex_weight
            );
            const matches_cut_but_better_balance = (
                running_cut === best_cut &&
                new_partition_diff < best_partition_diff
            );

            if (improves_cut_within_balance || matches_cut_but_better_balance) {
                best_cut = running_cut;
                best_partition_diff = new_partition_diff;
                best_cut_swap_index = swap_count;
            } else if (swap_count - best_cut_swap_index > limit_nonimproving_moves) {
                // Pass has gone too long without improvement — undo this last move and stop.
                running_cut += gain;
                partition_weights[from_side] += vertex_weights[high_gain_vertex];
                partition_weights[to_side] -= vertex_weights[high_gain_vertex];
                break;
            }

            vertex_partition[high_gain_vertex] = to_side;
            moved_pass_index[high_gain_vertex] = swap_count;
            swap_history[swap_count] = high_gain_vertex;

            // After the move, swap id/ed for the moved vertex:
            // edges that used to be external (going to `to_side`) are now internal,
            // and vice versa.
            {
                const tmp = internal_degree[high_gain_vertex];
                internal_degree[high_gain_vertex] = external_degree[high_gain_vertex];
                external_degree[high_gain_vertex] = tmp;
            }

            // Maybe drop the moved vertex from the boundary.
            if (external_degree[high_gain_vertex] === 0 && edge_addresses[high_gain_vertex] < edge_addresses[high_gain_vertex + 1]) {
                const bp = boundary_position_of_vertex[high_gain_vertex];
                if (bp !== -1) {
                    const last = boundary_count - 1;
                    if (bp !== last) {
                        const moved_to_bp = boundary_vertices[last];
                        boundary_vertices[bp] = moved_to_bp;
                        boundary_position_of_vertex[moved_to_bp] = bp;
                    }
                    boundary_position_of_vertex[high_gain_vertex] = -1;
                    boundary_count = last;
                }
            }

            // Update id/ed of neighbours, their boundary membership, and their queue positions.
            const edge_start = edge_addresses[high_gain_vertex];
            const edge_end = edge_addresses[high_gain_vertex + 1];
            for (let edge_offset = edge_start; edge_offset < edge_end; edge_offset++) {
                const neighbor = adjacency[edge_offset];
                const ewgt = edge_weights[edge_offset];
                const signed_ewgt = (to_side === vertex_partition[neighbor]) ? ewgt : -ewgt;
                internal_degree[neighbor] += signed_ewgt;
                external_degree[neighbor] -= signed_ewgt;

                const n_was_on_boundary = boundary_position_of_vertex[neighbor] !== -1;

                if (n_was_on_boundary) {
                    if (external_degree[neighbor] === 0) {
                        // Drop from boundary
                        const bp = boundary_position_of_vertex[neighbor];
                        const last = boundary_count - 1;
                        if (bp !== last) {
                            const moved_to_bp = boundary_vertices[last];
                            boundary_vertices[bp] = moved_to_bp;
                            boundary_position_of_vertex[moved_to_bp] = bp;
                        }
                        boundary_position_of_vertex[neighbor] = -1;
                        boundary_count = last;
                        if (moved_pass_index[neighbor] === -1) {
                            queues[vertex_partition[neighbor]].remove(neighbor);
                        }
                    } else {
                        // Still on boundary, update queue priority
                        if (moved_pass_index[neighbor] === -1) {
                            const updated_gain = external_degree[neighbor] - internal_degree[neighbor];
                            queues[vertex_partition[neighbor]].update(neighbor, updated_gain);
                        }
                    }
                } else {
                    if (external_degree[neighbor] > 0) {
                        // New boundary vertex
                        boundary_vertices[boundary_count] = neighbor;
                        boundary_position_of_vertex[neighbor] = boundary_count;
                        boundary_count++;
                        if (moved_pass_index[neighbor] === -1) {
                            const initial_gain = external_degree[neighbor] - internal_degree[neighbor];
                            queues[vertex_partition[neighbor]].insert(neighbor, initial_gain);
                        }
                    }
                }
            }
        }

        // Reset moved-status for vertices we touched this pass. Preserves the
        // "all -1" invariant for the shared moved_pass_index across calls.
        for (let i = 0; i < swap_count; i++) {
            moved_pass_index[swap_history[i]] = -1;
        }

        // Roll back any moves made after best_cut_swap_index.
        for (let i = swap_count - 1; i > best_cut_swap_index; i--) {
            const vertex = swap_history[i];
            const flipped_from = vertex_partition[vertex];
            const flipped_to = 1 - flipped_from;
            vertex_partition[vertex] = flipped_to;

            // Swap id/ed back
            {
                const tmp = internal_degree[vertex];
                internal_degree[vertex] = external_degree[vertex];
                external_degree[vertex] = tmp;
            }

            // Maybe re-add or remove from boundary based on new ed
            if (external_degree[vertex] === 0 && boundary_position_of_vertex[vertex] !== -1 && edge_addresses[vertex] < edge_addresses[vertex + 1]) {
                const bp = boundary_position_of_vertex[vertex];
                const last = boundary_count - 1;
                if (bp !== last) {
                    const moved_to_bp = boundary_vertices[last];
                    boundary_vertices[bp] = moved_to_bp;
                    boundary_position_of_vertex[moved_to_bp] = bp;
                }
                boundary_position_of_vertex[vertex] = -1;
                boundary_count = last;
            } else if (external_degree[vertex] > 0 && boundary_position_of_vertex[vertex] === -1) {
                boundary_vertices[boundary_count] = vertex;
                boundary_position_of_vertex[vertex] = boundary_count;
                boundary_count++;
            }

            partition_weights[flipped_from] -= vertex_weights[vertex];
            partition_weights[flipped_to] += vertex_weights[vertex];

            // Update neighbours
            const edge_start = edge_addresses[vertex];
            const edge_end = edge_addresses[vertex + 1];
            for (let edge_offset = edge_start; edge_offset < edge_end; edge_offset++) {
                const neighbor = adjacency[edge_offset];
                const ewgt = edge_weights[edge_offset];
                const signed_ewgt = (flipped_to === vertex_partition[neighbor]) ? ewgt : -ewgt;
                internal_degree[neighbor] += signed_ewgt;
                external_degree[neighbor] -= signed_ewgt;

                if (boundary_position_of_vertex[neighbor] !== -1 && external_degree[neighbor] === 0) {
                    const bp = boundary_position_of_vertex[neighbor];
                    const last = boundary_count - 1;
                    if (bp !== last) {
                        const moved_to_bp = boundary_vertices[last];
                        boundary_vertices[bp] = moved_to_bp;
                        boundary_position_of_vertex[moved_to_bp] = bp;
                    }
                    boundary_position_of_vertex[neighbor] = -1;
                    boundary_count = last;
                }
                if (boundary_position_of_vertex[neighbor] === -1 && external_degree[neighbor] > 0) {
                    boundary_vertices[boundary_count] = neighbor;
                    boundary_position_of_vertex[neighbor] = boundary_count;
                    boundary_count++;
                }
            }
        }

        graph.current_cut = best_cut;
        graph.boundary_count = boundary_count;

        if (best_cut_swap_index <= 0 || best_cut === initial_cut_this_pass) {
            break;
        }
    }
}
