import { create_coarse_graph } from "./create_coarse_graph.js";
import { match_shem } from "./match_shem.js";

/**
 * If contraction shrinks the graph by less than this much in a level, further
 * coarsening probably won't help. Matches the COARSEN_FRACTION threshold in
 * libmetis/defs.h.
 */
const MIN_USEFUL_CONTRACTION_RATIO = 0.85;

/**
 * Safety cap: if we somehow keep contracting forever (shouldn't happen, but
 * stops a runaway loop in pathological inputs).
 */
const MAX_COARSENING_LEVELS = 32;

/**
 * Drive the coarsening phase: repeatedly match + contract until the coarsest
 * graph is small enough, or the contraction ratio drops below the threshold.
 *
 * On entry, `graph` is the original finest graph. On return, the result is the
 * coarsest graph; walking `.finer_graph` repeatedly reaches the original.
 *
 * Mirrors `CoarsenGraph` from libmetis/coarsen.c.
 *
 * @param {import('../MetisGraph.js').MetisGraph} graph
 * @param {{random: function():number, coarsen_until_vertex_count: number}} opts
 *   `coarsen_until_vertex_count`: stop coarsening once vertex_count drops at or
 *   below this. METIS uses `max(vertex_count / (20·log2(nparts)), 30·nparts)`.
 * @returns {import('../MetisGraph.js').MetisGraph} the coarsest graph
 */
export function coarsen_graph(graph, opts) {
    const random = opts.random;
    const coarsen_until_vertex_count = opts.coarsen_until_vertex_count;

    let current_level = graph;
    let remaining_levels = MAX_COARSENING_LEVELS;

    // Three vertex_count-sized buffers reused across every coarsening level.
    // Sized to the finest graph's vertex_count; subsequent (coarser) levels
    // only need a prefix. match_shem either refills the active range itself
    // (`partner_of_vertex`) or overwrites every entry it reads
    // (`visit_order` and `sort_key_degree`), so stale tail data is harmless.
    const partner_of_vertex = new Uint32Array(graph.vertex_count);
    const visit_order = new Uint32Array(graph.vertex_count);
    const sort_key_degree = new Uint32Array(graph.vertex_count);

    while (remaining_levels-- > 0) {

        // Cap a single coarse vertex's weight at ~1.5x what one balanced
        // partition could hold at the coarsest level. METIS uses
        // 1.5 * total_vertex_weight / coarsen_until_vertex_count.
        // The total is preserved across coarsening, so recomputing per level
        // is cheap and follows the size threshold naturally.
        const max_coarse_vertex_weight = Math.max(
            1,
            Math.ceil(1.5 * current_level.total_vertex_weight / coarsen_until_vertex_count)
        );

        const coarse_vertex_count = match_shem(
            current_level,
            random,
            max_coarse_vertex_weight,
            partner_of_vertex,
            visit_order,
            sort_key_degree
        );

        if (coarse_vertex_count >= current_level.vertex_count) {
            // Matching collapsed nothing — graph is too irregular to benefit
            // from another level. Stop.
            break;
        }

        const coarser = create_coarse_graph(current_level, coarse_vertex_count, partner_of_vertex);
        const contraction_ratio = coarser.vertex_count / current_level.vertex_count;

        current_level = coarser;

        if (current_level.vertex_count <= coarsen_until_vertex_count) {
            break;
        }
        if (contraction_ratio > MIN_USEFUL_CONTRACTION_RATIO) {
            // Contraction stalled; further levels offer diminishing returns.
            break;
        }
        if (current_level.adjacency_length < current_level.vertex_count / 2) {
            // Graph is too sparse to benefit from another coarsening pass.
            break;
        }
    }

    return current_level;
}
