//! The [`MstConnect`] connector pass (spec §4.7).
//!
//! A *connector* decides **which regions should connect** — it does not carve a
//! single tile. Per the inter-pass data contract (see [`crate::passes`]), this
//! pass reads the real [`Room`](crate::region::RegionKind::Room) regions and
//! writes the connectivity [`Edge`](crate::region::Edge)s into `ctx.graph`, each
//! with `at: None` (the door cell is filled in later by `DoorPlacer`).
//!
//! A *real* room is a region with `kind == Room` **and** a non-empty `cells`
//! set. A `Room` with empty `cells` is an uncarved leaf that `RoomCarver`
//! skipped (see [`crate::passes::room`]); such placeholders are ignored here so
//! we never wire an edge to a room that does not physically exist.
//!
//! The connection plan is:
//!
//! 1. Build a **minimum spanning tree** over the room centers, with edge weight
//!    equal to the [Manhattan](crate::geometry::Point::manhattan) distance
//!    between centers. The MST has exactly `N - 1` edges for `N` rooms and
//!    guarantees the whole dungeon is reachable.
//! 2. Add a configurable fraction
//!    ([`extra_edge_ratio`](ConnectConfig::extra_edge_ratio)) of **extra short
//!    edges** — the next-shortest room-to-room pairs not already in the tree —
//!    so the dungeon gains loops instead of being a strict tree.
//!
//! ## Determinism (spec §7)
//!
//! Nothing here depends on `HashMap`/`HashSet` iteration order. Rooms are
//! collected in [`RegionId`](crate::region::RegionId) (index) order, the MST is
//! grown with Prim's algorithm breaking distance ties by the candidate's room
//! index, and the extra edges are chosen from a list sorted by `(weight, a, b)`.
//! Given the same `rng` sub-stream and the same room set, the emitted edge
//! sequence is byte-identical on every machine. (In v1 the plan is in fact fully
//! deterministic from the rooms alone; `rng` is accepted to honor the [`Pass`]
//! contract and leave room for future randomized loop selection.)

use serde::{Deserialize, Serialize};

use crate::pass::{GenContext, Pass};
use crate::region::{RegionId, RegionKind};
use crate::rng::Rng;

/// Configuration for an [`MstConnect`] pass.
///
/// `extra_edge_ratio` is the fraction of *extra* (non-tree) loop edges to add,
/// expressed relative to the spanning tree's edge count (`N - 1` for `N`
/// rooms). For example `0.25` over 9 rooms adds `floor(0.25 * 8) = 2` loop
/// edges on top of the 8 tree edges. `0.0` (the default) yields a pure
/// minimum spanning tree with no loops. Values are clamped to `>= 0.0` and the
/// extra count is capped at the number of available non-tree pairs, so an
/// out-of-range or huge ratio never panics.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct ConnectConfig {
    /// Fraction of extra loop edges to add, relative to the `N - 1` tree edges.
    /// `0.0` means a pure MST.
    pub extra_edge_ratio: f64,
}

impl Default for ConnectConfig {
    /// A sensible default for the v1 dungeon: a pure MST (no loop edges).
    fn default() -> Self {
        ConnectConfig {
            extra_edge_ratio: 0.0,
        }
    }
}

/// A minimum-spanning-tree connector pass (spec §4.7).
///
/// Reads the real [`Room`](crate::region::RegionKind::Room) regions, builds an
/// MST over their centers plus a configurable fraction of extra loop edges, and
/// records each link in `ctx.graph` (all with `at: None`). Carves no tiles.
/// Construct one with [`MstConnect::new`]; it implements [`Pass`] with the
/// stable name `"mst_connect"`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MstConnect {
    /// The loop-density configuration this connector plans under.
    cfg: ConnectConfig,
}

impl MstConnect {
    /// Creates a connector with the given configuration.
    pub fn new(cfg: ConnectConfig) -> Self {
        MstConnect { cfg }
    }
}

impl Pass for MstConnect {
    fn name(&self) -> &str {
        "mst_connect"
    }

    fn apply(&self, ctx: &mut GenContext, _rng: &mut Rng) {
        // Collect the *real* rooms in RegionId (index) order: kind == Room AND a
        // non-empty cell set. An empty-`cells` Room is an uncarved leaf and is
        // skipped, so we never connect to a room that does not exist. Index order
        // is the deterministic, hash-free basis for everything below.
        let rooms: Vec<RoomNode> = ctx
            .regions
            .iter()
            .filter(|r| r.kind == RegionKind::Room && !r.cells.is_empty())
            .map(|r| RoomNode {
                id: r.id,
                center: r.bounds.center(),
            })
            .collect();

        let n = rooms.len();
        // Zero or one room: nothing to connect. No edges, no panic.
        if n < 2 {
            return;
        }

        // 1. Minimum spanning tree over room centers (Prim's algorithm). The set
        //    of tree edges is recorded as pairs of *room-list indices* (not
        //    RegionIds) so we can cheaply test membership when choosing extras.
        let tree = build_mst(&rooms);

        // 2. Extra loop edges: the shortest room-to-room pairs not already in the
        //    tree, up to `floor(ratio * (n - 1))` of them.
        let tree_count = tree.len(); // == n - 1
        let ratio = self.cfg.extra_edge_ratio.max(0.0);
        let want_extra = (ratio * tree_count as f64).floor() as usize;
        let extras = pick_extra_edges(&rooms, &tree, want_extra);

        // Emit tree edges first (in the order Prim's discovered them), then the
        // extra loop edges (shortest first). Every endpoint is a valid Room
        // RegionId; `add_edge` stores `at: None`.
        for &(i, j) in tree.iter().chain(extras.iter()) {
            ctx.graph.add_edge(rooms[i].id, rooms[j].id);
        }
    }
}

/// A room reduced to what the connector needs: its [`RegionId`] and its center.
#[derive(Clone, Copy, Debug)]
struct RoomNode {
    /// The room's region id (used as the edge endpoint).
    id: RegionId,
    /// The room's center cell, used to weight edges by Manhattan distance.
    center: crate::geometry::Point,
}

/// Manhattan distance between two rooms' centers (the edge weight).
fn weight(rooms: &[RoomNode], i: usize, j: usize) -> i32 {
    rooms[i].center.manhattan(rooms[j].center)
}

/// Builds a minimum spanning tree over `rooms` (length `>= 2`) with Prim's
/// algorithm, returning its `n - 1` edges as `(i, j)` room-list index pairs in
/// the order they were added.
///
/// Determinism: the tree is grown from room index 0; at each step the
/// closest not-yet-connected room is chosen, breaking distance ties by the
/// smaller room index. No hashing or set iteration influences the result.
fn build_mst(rooms: &[RoomNode]) -> Vec<(usize, usize)> {
    let n = rooms.len();
    let mut in_tree = vec![false; n];
    // For each outside room, the cheapest edge connecting it to the tree:
    // `best_dist[k]` is its distance to `best_from[k]` (a room already in tree).
    let mut best_dist = vec![i32::MAX; n];
    let mut best_from = vec![0usize; n];
    let mut edges = Vec::with_capacity(n - 1);

    // Seed the tree with room 0.
    in_tree[0] = true;
    for k in 0..n {
        if k != 0 {
            best_dist[k] = weight(rooms, 0, k);
            best_from[k] = 0;
        }
    }

    // Add the remaining n - 1 rooms one at a time, each via its cheapest link.
    for _ in 1..n {
        // Pick the outside room with the smallest connecting distance; ties go to
        // the smaller index (the `<` comparison only updates on a strict
        // improvement, and we scan indices ascending).
        let mut next = None;
        let mut next_dist = i32::MAX;
        for k in 0..n {
            if !in_tree[k] && best_dist[k] < next_dist {
                next_dist = best_dist[k];
                next = Some(k);
            }
        }
        // `rooms` is connected by construction (complete graph), so `next` is
        // always `Some` here; the `let-else` keeps the function total regardless.
        let Some(k) = next else { break };

        in_tree[k] = true;
        edges.push((best_from[k], k));

        // Relax the outside rooms against the newly added room `k`.
        for m in 0..n {
            if !in_tree[m] {
                let d = weight(rooms, k, m);
                if d < best_dist[m] {
                    best_dist[m] = d;
                    best_from[m] = k;
                }
            }
        }
    }

    edges
}

/// Chooses up to `want` extra loop edges: the shortest room pairs not already in
/// `tree`, deduplicated and ordered by `(weight, a, b)` room indices.
///
/// Returns `(i, j)` room-list index pairs with `i < j`. The deterministic sort
/// key means the same room set always yields the same loop edges.
fn pick_extra_edges(
    rooms: &[RoomNode],
    tree: &[(usize, usize)],
    want: usize,
) -> Vec<(usize, usize)> {
    if want == 0 {
        return Vec::new();
    }
    let n = rooms.len();

    // Normalize tree edges to `(min, max)` so membership testing is orientation
    // independent. A small sorted Vec is enough (n is tiny) and avoids any hash
    // set / iteration-order dependence.
    let mut tree_pairs: Vec<(usize, usize)> = tree
        .iter()
        .map(|&(a, b)| if a < b { (a, b) } else { (b, a) })
        .collect();
    tree_pairs.sort_unstable();

    // Every candidate pair `i < j` that is not a tree edge, with its weight.
    let mut candidates: Vec<(i32, usize, usize)> = Vec::new();
    for i in 0..n {
        for j in (i + 1)..n {
            if tree_pairs.binary_search(&(i, j)).is_err() {
                candidates.push((weight(rooms, i, j), i, j));
            }
        }
    }
    // Shortest first; ties broken by (a, b) index for a stable, portable order.
    candidates.sort_unstable();

    candidates
        .into_iter()
        .take(want)
        .map(|(_, i, j)| (i, j))
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::blackboard::Blackboard;
    use crate::geometry::{Point, Rect};
    use crate::grid::Grid;
    use crate::map::Tile;
    use crate::region::{ConnGraph, Region, RegionId, RegionKind};

    /// Builds a `GenContext` in this pass's required precondition: a set of
    /// *real* Room regions (kind = Room, non-empty cells). Each room is given a
    /// 2x2 footprint at the supplied top-left so its `bounds.center()` is stable.
    /// Constructed by hand so the test depends on no other pass.
    ///
    /// The grid is all-`Wall`; this pass carves nothing, so its contents only
    /// matter for the "carves no tiles" assertion.
    fn ctx_with_rooms(width: u32, height: u32, room_origins: &[(i32, i32)]) -> GenContext {
        let mut ctx = GenContext {
            tiles: Grid::new(width, height, Tile::Wall),
            regions: Vec::new(),
            graph: ConnGraph::new(),
            blackboard: Blackboard::new(),
        };
        for &(x, y) in room_origins {
            let bounds = Rect::new(x, y, 2, 2);
            let cells: Vec<Point> = bounds.iter().collect();
            ctx.add_region(RegionKind::Room, bounds, cells);
        }
        ctx
    }

    /// Runs `MstConnect` with the given config/seed over `ctx`, keyed exactly as
    /// the pipeline would (`"mst_connect#0"` sub-stream).
    fn run(ctx: &mut GenContext, cfg: ConnectConfig, seed: u64) {
        let mut rng = Rng::from_seed(seed).fork("mst_connect#0");
        MstConnect::new(cfg).apply(ctx, &mut rng);
    }

    /// Union-find over a set of edges, used to verify the graph is connected.
    struct UnionFind {
        parent: Vec<usize>,
    }
    impl UnionFind {
        fn new(n: usize) -> Self {
            UnionFind {
                parent: (0..n).collect(),
            }
        }
        fn find(&mut self, mut x: usize) -> usize {
            while self.parent[x] != x {
                self.parent[x] = self.parent[self.parent[x]];
                x = self.parent[x];
            }
            x
        }
        fn union(&mut self, a: usize, b: usize) {
            let (ra, rb) = (self.find(a), self.find(b));
            if ra != rb {
                self.parent[ra] = rb;
            }
        }
        fn components(&mut self, n: usize) -> usize {
            let roots: std::collections::BTreeSet<usize> =
                (0..n).map(|i| self.find(i)).collect();
            roots.len()
        }
    }

    /// The pass identifies itself with the exact contract name.
    #[test]
    fn name_is_mst_connect() {
        assert_eq!(
            MstConnect::new(ConnectConfig::default()).name(),
            "mst_connect"
        );
    }

    /// A pure MST (`extra_edge_ratio: 0.0`) over N rooms has exactly N-1 edges,
    /// connects every room, and every edge has `at == None` referencing valid
    /// Room region ids.
    #[test]
    fn pure_mst_has_n_minus_one_edges_and_is_connected() {
        // 5 rooms scattered so distances differ.
        let origins = [(0, 0), (20, 0), (0, 20), (20, 20), (10, 10)];
        let mut ctx = ctx_with_rooms(40, 40, &origins);
        run(&mut ctx, ConnectConfig::default(), 0xABCD);

        let n = origins.len();
        let edges = ctx.graph.edges();
        assert_eq!(edges.len(), n - 1, "a pure MST over N rooms has N-1 edges");

        // Every edge endpoint is a valid Room region id, and `at` is None.
        let room_ids: std::collections::BTreeSet<RegionId> = ctx
            .regions
            .iter()
            .filter(|r| r.kind == RegionKind::Room && !r.cells.is_empty())
            .map(|r| r.id)
            .collect();
        for e in edges {
            assert_eq!(e.at, None, "MstConnect leaves the door location unknown");
            assert!(room_ids.contains(&e.a), "edge endpoint {:?} not a room", e.a);
            assert!(room_ids.contains(&e.b), "edge endpoint {:?} not a room", e.b);
            assert_ne!(e.a, e.b, "no self-loops");
        }

        // The graph connects all N rooms (single component over the edges).
        let mut uf = UnionFind::new(n);
        for e in edges {
            uf.union(e.a.0, e.b.0);
        }
        assert_eq!(uf.components(n), 1, "MST must connect every room");
    }

    /// A positive `extra_edge_ratio` adds the expected number of loop edges on
    /// top of the N-1 tree edges, and the graph stays connected.
    #[test]
    fn extra_ratio_adds_expected_loop_edges() {
        // 5 rooms => tree has 4 edges. ratio 0.5 => floor(0.5 * 4) = 2 extras.
        let origins = [(0, 0), (20, 0), (0, 20), (20, 20), (10, 10)];
        let mut ctx = ctx_with_rooms(40, 40, &origins);
        run(&mut ctx, ConnectConfig { extra_edge_ratio: 0.5 }, 0x1234);

        let n = origins.len();
        let expected = (n - 1) + 2;
        assert_eq!(
            ctx.graph.edges().len(),
            expected,
            "expected {} tree + 2 extra edges",
            n - 1
        );

        // Still connected, still all `at: None`, still room-to-room.
        let mut uf = UnionFind::new(n);
        for e in ctx.graph.edges() {
            assert_eq!(e.at, None);
            uf.union(e.a.0, e.b.0);
        }
        assert_eq!(uf.components(n), 1);
    }

    /// `extra_edge_ratio: 0.0` produces exactly N-1 edges (no loops).
    #[test]
    fn zero_ratio_is_pure_mst() {
        let origins = [(0, 0), (10, 0), (0, 10), (10, 10)];
        let mut ctx = ctx_with_rooms(20, 20, &origins);
        run(&mut ctx, ConnectConfig { extra_edge_ratio: 0.0 }, 7);
        assert_eq!(ctx.graph.edges().len(), origins.len() - 1);
    }

    /// A huge ratio is capped at the number of available non-tree pairs and never
    /// panics: with 4 rooms there are C(4,2)=6 possible pairs, 3 in the tree, so
    /// at most 3 extras can be added regardless of the ratio.
    #[test]
    fn oversized_ratio_is_capped_without_panic() {
        let origins = [(0, 0), (10, 0), (0, 10), (10, 10)];
        let mut ctx = ctx_with_rooms(20, 20, &origins);
        run(
            &mut ctx,
            ConnectConfig {
                extra_edge_ratio: 1000.0,
            },
            99,
        );
        // 6 total pairs is the hard ceiling on edges over 4 rooms.
        assert!(ctx.graph.edges().len() <= 6, "edge count cannot exceed all pairs");
        assert_eq!(ctx.graph.edges().len(), 6, "all 6 pairs added when ratio is huge");
    }

    /// A single room (N=1) produces zero edges without panic.
    #[test]
    fn single_room_produces_no_edges() {
        let mut ctx = ctx_with_rooms(10, 10, &[(2, 2)]);
        run(&mut ctx, ConnectConfig::default(), 0);
        assert!(ctx.graph.edges().is_empty(), "one room has nothing to connect");
    }

    /// Zero rooms produces zero edges without panic.
    #[test]
    fn no_rooms_produces_no_edges() {
        let mut ctx = ctx_with_rooms(10, 10, &[]);
        run(&mut ctx, ConnectConfig::default(), 0);
        assert!(ctx.graph.edges().is_empty());
    }

    /// Empty-`cells` Room placeholders (uncarved leaves) and non-Room regions are
    /// ignored: only real rooms are connected, and edges reference only real-room
    /// ids.
    #[test]
    fn ignores_uncarved_leaves_and_non_rooms() {
        let mut ctx = GenContext {
            tiles: Grid::new(40, 40, Tile::Wall),
            regions: Vec::new(),
            graph: ConnGraph::new(),
            blackboard: Blackboard::new(),
        };
        // Two real rooms (ids 0 and 2) bracketing an uncarved leaf (id 1) and a
        // corridor (id 3), so the surviving ids are non-contiguous.
        let r0 = Rect::new(0, 0, 2, 2);
        ctx.add_region(RegionKind::Room, r0, r0.iter().collect()); // id 0: real
        ctx.add_region(RegionKind::Room, Rect::new(10, 0, 4, 4), Vec::new()); // id 1: uncarved leaf
        let r2 = Rect::new(20, 20, 2, 2);
        ctx.add_region(RegionKind::Room, r2, r2.iter().collect()); // id 2: real
        ctx.regions.push(Region {
            id: RegionId(3),
            kind: RegionKind::Corridor,
            bounds: Rect::new(30, 0, 4, 1),
            cells: vec![Point::new(30, 0)],
            theme: None,
        });

        run(&mut ctx, ConnectConfig::default(), 0xBEEF);

        // Two real rooms => exactly one tree edge, joining ids 0 and 2.
        let edges = ctx.graph.edges();
        assert_eq!(edges.len(), 1, "two real rooms => one edge");
        let e = edges[0];
        let ends = [e.a, e.b];
        assert!(ends.contains(&RegionId(0)) && ends.contains(&RegionId(2)));
        assert!(
            !ends.contains(&RegionId(1)) && !ends.contains(&RegionId(3)),
            "must not connect an uncarved leaf or a corridor"
        );
    }

    /// Carves no tiles: the grid is untouched (all Wall) after running.
    #[test]
    fn carves_no_tiles() {
        let origins = [(0, 0), (20, 0), (0, 20), (20, 20)];
        let mut ctx = ctx_with_rooms(40, 40, &origins);
        run(&mut ctx, ConnectConfig { extra_edge_ratio: 0.5 }, 5);
        assert!(
            ctx.tiles.iter().all(|(_, &t)| t == Tile::Wall),
            "a connector must not carve any tile"
        );
    }

    /// Determinism: the same seed/sub-stream and room set produce an identical
    /// edge sequence (endpoints in the same order).
    #[test]
    fn same_seed_yields_identical_edges() {
        let origins = [(0, 0), (15, 3), (4, 18), (22, 19), (9, 9), (30, 5)];
        let cfg = ConnectConfig { extra_edge_ratio: 0.5 };

        let mut a = ctx_with_rooms(40, 40, &origins);
        let mut b = ctx_with_rooms(40, 40, &origins);
        run(&mut a, cfg, 0x5EED);
        run(&mut b, cfg, 0x5EED);

        let edges_a: Vec<(RegionId, RegionId)> =
            a.graph.edges().iter().map(|e| (e.a, e.b)).collect();
        let edges_b: Vec<(RegionId, RegionId)> =
            b.graph.edges().iter().map(|e| (e.a, e.b)).collect();
        assert_eq!(edges_a, edges_b, "same seed must reproduce the same edge set");
    }

    /// Determinism is independent of `rng` in v1: the plan is derived purely from
    /// the rooms, so two different seeds yield the same edges. (This documents the
    /// current behavior; it also proves no hidden hash-iteration influence.)
    #[test]
    fn plan_is_rng_independent_in_v1() {
        let origins = [(0, 0), (15, 3), (4, 18), (22, 19), (9, 9)];
        let cfg = ConnectConfig { extra_edge_ratio: 0.25 };

        let mut a = ctx_with_rooms(40, 40, &origins);
        let mut b = ctx_with_rooms(40, 40, &origins);
        run(&mut a, cfg, 1);
        run(&mut b, cfg, 999_999);

        let edges_a: Vec<(RegionId, RegionId)> =
            a.graph.edges().iter().map(|e| (e.a, e.b)).collect();
        let edges_b: Vec<(RegionId, RegionId)> =
            b.graph.edges().iter().map(|e| (e.a, e.b)).collect();
        assert_eq!(edges_a, edges_b);
    }
}
