//! The [`CorridorCarver`] connector pass (spec §4.7).
//!
//! A *connector* in its carving role: `MstConnect` decides **which** rooms link
//! up (the [`Edge`](crate::region::Edge)s in `ctx.graph`); this pass makes those
//! links physical by carving a corridor of [`Floor`](crate::map::Tile::Floor)
//! between each edge's two room centers.
//!
//! Per the inter-pass data contract (see [`crate::passes`]), for every edge in
//! `ctx.graph` this pass:
//!
//! 1. Looks up the two endpoint [`Room`](crate::region::RegionKind::Room)
//!    regions (an edge endpoint [`RegionId`](crate::region::RegionId) is its
//!    index in `ctx.regions`) and takes each room's
//!    [`center`](crate::geometry::Rect::center).
//! 2. Carves an **L-shaped** corridor between the two centers: one horizontal run
//!    and one vertical run joined at an elbow. Which leg comes first (horizontal
//!    then vertical, or vertical then horizontal) is chosen from `rng`, so the
//!    elbow's side varies with the seed. Both legs are walked with
//!    [`Line::cells`](crate::geometry::Line::cells) and written through the
//!    bounds-safe [`Grid::set`](crate::grid::Grid::set) — any cell outside the
//!    grid is silently skipped, never a panic (spec §8).
//! 3. Appends one [`Corridor`](crate::region::RegionKind::Corridor) region whose
//!    `cells` are exactly the carved corridor cells (in carve order, the elbow
//!    counted once) and whose `bounds` is their axis-aligned bounding box.
//!
//! Carving over a cell that is already `Floor` (a room interior, or where two
//! corridors cross) is fine — corridors may pass through or merge.
//!
//! This pass **does not** set [`Door`](crate::map::Tile::Door) tiles and **does
//! not** touch any `edge.at`; that is `DoorPlacer`'s job. It reads `ctx.graph`
//! and the Room region centers and writes `ctx.tiles` plus one Corridor region
//! per edge.
//!
//! ## Determinism (spec §7)
//!
//! Edges are processed in `ctx.graph`'s insertion order (the order
//! [`ConnGraph::edges`](crate::region::ConnGraph::edges) yields), and the only
//! randomness is the per-edge leg-order coin flip drawn from the passed `rng` in
//! that same order. Nothing depends on `HashMap`/`HashSet` iteration. Given the
//! same `rng` sub-stream and the same graph + rooms, the carved tiles and
//! Corridor regions are byte-identical on every machine. Its tests construct the
//! precondition (rooms + a graph) directly, so this pass depends on no other
//! pass's code.

use crate::geometry::{Line, Point, Rect};
use crate::map::Tile;
use crate::pass::{GenContext, Pass};
use crate::region::RegionKind;
use crate::rng::Rng;

/// A corridor-carving connector pass (spec §4.7).
///
/// Carves an L-shaped [`Floor`](crate::map::Tile::Floor) corridor for every
/// edge in `ctx.graph`, joining the two rooms' centers, and records each as a
/// [`Corridor`](crate::region::RegionKind::Corridor) region. Construct one with
/// [`CorridorCarver::new`] (or [`Default`]); it implements [`Pass`] with the
/// stable name `"corridor_carver"`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CorridorCarver;

impl CorridorCarver {
    /// Creates a corridor carver.
    pub fn new() -> Self {
        CorridorCarver
    }
}

impl Pass for CorridorCarver {
    fn name(&self) -> &str {
        "corridor_carver"
    }

    fn apply(&self, ctx: &mut GenContext, rng: &mut Rng) {
        // Snapshot each edge's two endpoint centers up front, in graph insertion
        // order. We read both `ctx.graph` and `ctx.regions` here, then drop those
        // borrows before carving — so the carve loop is free to call
        // `ctx.add_region` (which needs `&mut ctx`) without a borrow conflict.
        //
        // An endpoint RegionId is its index in `ctx.regions` (the id-as-index
        // invariant). A missing or non-Room endpoint yields `None` and that edge
        // is skipped, keeping the pass total even on a malformed graph.
        let endpoints: Vec<(Point, Point)> = ctx
            .graph
            .edges()
            .iter()
            .filter_map(|edge| {
                let a = room_center(ctx, edge.a)?;
                let b = room_center(ctx, edge.b)?;
                Some((a, b))
            })
            .collect();

        for (from, to) in endpoints {
            // Choose which leg leads: a horizontal-first L bends at (to.x, from.y),
            // a vertical-first L bends at (from.x, to.y). The coin flip is the only
            // randomness, drawn once per edge in graph order for determinism.
            let horizontal_first = rng.chance(0.5);
            let elbow = if horizontal_first {
                Point::new(to.x, from.y)
            } else {
                Point::new(from.x, to.y)
            };

            // Walk both legs (from -> elbow -> to), carving Floor and recording
            // each carved cell once. The elbow is the last cell of the first leg
            // and the first cell of the second, so we skip the second leg's
            // leading cell to avoid duplicating it.
            let mut cells: Vec<Point> = Vec::new();
            for p in Line::new(from, elbow).cells() {
                carve(ctx, p, &mut cells);
            }
            for p in Line::new(elbow, to).cells().skip(1) {
                carve(ctx, p, &mut cells);
            }

            // Record the corridor region (one per edge). Bounds is the AABB of the
            // carved cells; an empty cell set (degenerate) gets an empty rect.
            let bounds = bounding_rect(&cells);
            ctx.add_region(RegionKind::Corridor, bounds, cells);
        }
    }
}

/// Returns a corridor anchor for the `Room` region with id `index`, or [`None`]
/// if the id is out of range or names a non-Room region.
///
/// The anchor is the room's carved cell **nearest its bounding-box center**. For
/// a convex, centered room (rectangle, octagon, ellipse, plus) the center cell is
/// itself carved, so this is exactly `bounds.center()` — i.e. output-preserving.
/// For an irregular room whose AABB center may fall in rock (a cave blob), it
/// snaps to the nearest actual floor cell, so the corridor always meets carved
/// floor and the room cannot be left unreachable.
///
/// Endpoint ids come from `MstConnect`, which only ever links real rooms (with
/// non-empty `cells`), so in the assembled recipe this always resolves to a real
/// cell; the guards keep the pass total against a hand-built or malformed graph.
fn room_center(ctx: &GenContext, index: crate::region::RegionId) -> Option<Point> {
    let region = ctx.regions.get(index.0)?;
    if region.kind != RegionKind::Room {
        return None;
    }
    let target = region.bounds.center();
    region
        .cells
        .iter()
        .copied()
        .min_by_key(|p| (p.x - target.x).pow(2) + (p.y - target.y).pow(2))
        .or(Some(target))
}

/// Carves [`Floor`](Tile::Floor) at `p` (a bounds-safe no-op if `p` is off-grid)
/// and records `p` as a corridor cell.
///
/// The cell is recorded whether or not it was already `Floor`, so a corridor's
/// `cells` faithfully describe the full L-path even where it overlaps a room or
/// another corridor. We push regardless of in-bounds-ness so `cells` matches the
/// geometric path; downstream consumers already treat off-grid cells as no-ops.
fn carve(ctx: &mut GenContext, p: Point, cells: &mut Vec<Point>) {
    ctx.tiles.set(p, Tile::Floor);
    cells.push(p);
}

/// Computes the axis-aligned bounding box enclosing every point in `cells`.
///
/// Returns an empty rect (`w == h == 0`) when `cells` is empty, so the result is
/// always well-formed. For one cell the rect is a 1x1 box at that cell.
fn bounding_rect(cells: &[Point]) -> Rect {
    let Some(&first) = cells.first() else {
        return Rect::new(0, 0, 0, 0);
    };
    let (mut min_x, mut min_y) = (first.x, first.y);
    let (mut max_x, mut max_y) = (first.x, first.y);
    for &p in &cells[1..] {
        min_x = min_x.min(p.x);
        min_y = min_y.min(p.y);
        max_x = max_x.max(p.x);
        max_y = max_y.max(p.y);
    }
    // +1 on each extent: bounds are inclusive of both the min and max cell.
    Rect::new(min_x, min_y, max_x - min_x + 1, max_y - min_y + 1)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::blackboard::Blackboard;
    use crate::grid::Grid;
    use crate::region::{ConnGraph, RegionId, RegionKind};
    use std::collections::{BTreeSet, VecDeque};

    /// Builds a `GenContext` in this pass's required precondition: an all-`Wall`
    /// grid with a set of *real* Room regions (each a carved Floor rect) plus a
    /// `ConnGraph` whose edges name those rooms by id. Constructed by hand so the
    /// test depends on no other pass.
    ///
    /// `rooms` are `(x, y, w, h)` rects carved to Floor; `edges` are
    /// `(room_index, room_index)` pairs added to the graph in order.
    fn ctx_with_rooms_and_edges(
        width: u32,
        height: u32,
        rooms: &[(i32, i32, i32, i32)],
        edges: &[(usize, usize)],
    ) -> GenContext {
        let mut ctx = GenContext {
            tiles: Grid::new(width, height, Tile::Wall),
            regions: Vec::new(),
            graph: ConnGraph::new(),
            blackboard: Blackboard::new(),
        };
        for &(x, y, w, h) in rooms {
            let bounds = Rect::new(x, y, w, h);
            let cells: Vec<Point> = bounds.iter().collect();
            // Carve the room's Floor so room centers start as Floor, exactly as
            // RoomCarver would leave them.
            for p in bounds.iter() {
                ctx.tiles.set(p, Tile::Floor);
            }
            ctx.add_region(RegionKind::Room, bounds, cells);
        }
        for &(a, b) in edges {
            ctx.graph.add_edge(RegionId(a), RegionId(b));
        }
        ctx
    }

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

    /// Flood-fills the 4-connected Floor component containing `start` and reports
    /// whether `goal` is in it. Used to verify two rooms are floor-connected.
    fn floor_connected(tiles: &Grid<Tile>, start: Point, goal: Point) -> bool {
        if tiles.get(start) != Some(&Tile::Floor) || tiles.get(goal) != Some(&Tile::Floor) {
            return false;
        }
        let mut seen: BTreeSet<(i32, i32)> = BTreeSet::new();
        let mut queue: VecDeque<Point> = VecDeque::new();
        seen.insert((start.x, start.y));
        queue.push_back(start);
        while let Some(p) = queue.pop_front() {
            if p == goal {
                return true;
            }
            for n in tiles.neighbors4(p) {
                if tiles.get(n) == Some(&Tile::Floor) && seen.insert((n.x, n.y)) {
                    queue.push_back(n);
                }
            }
        }
        false
    }

    /// The pass identifies itself with the exact contract name.
    #[test]
    fn name_is_corridor_carver() {
        assert_eq!(CorridorCarver::new().name(), "corridor_carver");
    }

    /// Given two rooms and one edge between them, after running the two room
    /// centers lie in the same connected Floor component (the L-corridor bridges
    /// them).
    #[test]
    fn one_edge_connects_two_room_centers() {
        // Two 4x4 rooms with a wall gap between them, one edge linking them.
        let rooms = [(2, 2, 4, 4), (16, 12, 4, 4)];
        let mut ctx = ctx_with_rooms_and_edges(24, 24, &rooms, &[(0, 1)]);

        let c0 = ctx.regions[0].bounds.center();
        let c1 = ctx.regions[1].bounds.center();
        // Before carving they are in separate components.
        assert!(!floor_connected(&ctx.tiles, c0, c1));

        run(&mut ctx, 0xABCD);

        // After carving the centers share one Floor component.
        assert!(
            floor_connected(&ctx.tiles, c0, c1),
            "the corridor must connect the two room centers"
        );
    }

    /// Exactly one Corridor region is added per edge, and every other region is
    /// preserved at its index (id-as-index invariant intact).
    #[test]
    fn one_corridor_region_per_edge() {
        let rooms = [(0, 0, 4, 4), (20, 0, 4, 4), (0, 20, 4, 4)];
        // Three edges => three corridor regions.
        let edges = [(0, 1), (1, 2), (0, 2)];
        let mut ctx = ctx_with_rooms_and_edges(24, 24, &rooms, &edges);
        run(&mut ctx, 0x1234);

        // 3 rooms + 3 corridors.
        assert_eq!(ctx.regions.len(), rooms.len() + edges.len());

        let corridors: Vec<&_> = ctx
            .regions
            .iter()
            .filter(|r| r.kind == RegionKind::Corridor)
            .collect();
        assert_eq!(corridors.len(), edges.len(), "one corridor region per edge");

        // The rooms are untouched and ids still equal indices.
        for (i, r) in ctx.regions.iter().enumerate() {
            assert_eq!(r.id, RegionId(i), "id must equal index");
        }
        for r in &corridors {
            assert!(!r.cells.is_empty(), "a carved corridor has cells");
            // Every recorded cell is in fact Floor (in-bounds ones at least).
            for &p in &r.cells {
                if ctx.tiles.in_bounds(p) {
                    assert_eq!(ctx.tiles.get(p), Some(&Tile::Floor));
                }
            }
            // Bounds enclose every cell.
            for &p in &r.cells {
                assert!(r.bounds.contains(p), "cell {p:?} escapes bounds {:?}", r.bounds);
            }
        }
    }

    /// An L-corridor's cells stay in bounds for interior rooms, and carving never
    /// panics even when a room center sits right at the map edge (legs run off the
    /// grid and the writes silently clip).
    #[test]
    fn carving_near_edge_does_not_panic_and_clips() {
        // Room A's center is at the very top-left corner; room B near the far
        // edge. The L between them hugs the boundary; off-grid writes are no-ops.
        let rooms = [(0, 0, 1, 1), (9, 9, 1, 1)];
        let mut ctx = ctx_with_rooms_and_edges(10, 10, &rooms, &[(0, 1)]);
        run(&mut ctx, 0xDEAD);

        // No panic above. Every recorded corridor cell that is in-bounds is Floor,
        // and no Floor was written outside the grid (the grid only has in-grid
        // cells to check).
        let corridor = ctx
            .regions
            .iter()
            .find(|r| r.kind == RegionKind::Corridor)
            .expect("one corridor region");
        for &p in &corridor.cells {
            if ctx.tiles.in_bounds(p) {
                assert_eq!(ctx.tiles.get(p), Some(&Tile::Floor));
            }
        }
    }

    /// A center literally on the boundary: the run extends off-grid but the carve
    /// clips with no panic and still records the geometric path.
    #[test]
    fn endpoint_outside_carves_without_panic() {
        // Room at the top-left 1x1; an edge to a room whose center is also at the
        // edge. Then shrink the grid mentally: the legs walk along x=0 / y=0.
        let rooms = [(0, 0, 1, 1), (0, 8, 1, 1)];
        let mut ctx = ctx_with_rooms_and_edges(9, 9, &rooms, &[(0, 1)]);
        // Should not panic.
        run(&mut ctx, 1);
        // Centers connected: both at x=0, a straight vertical run.
        let c0 = ctx.regions[0].bounds.center();
        let c1 = ctx.regions[1].bounds.center();
        assert!(floor_connected(&ctx.tiles, c0, c1));
    }

    /// Determinism: the same seed/sub-stream and the same graph + rooms reproduce
    /// identical carved tiles and identical Corridor regions.
    #[test]
    fn same_seed_yields_identical_corridors() {
        let rooms = [
            (2, 2, 4, 4),
            (24, 4, 4, 4),
            (6, 26, 4, 4),
            (28, 28, 4, 4),
        ];
        let edges = [(0, 1), (1, 3), (0, 2), (2, 3)];

        let mut a = ctx_with_rooms_and_edges(36, 36, &rooms, &edges);
        let mut b = ctx_with_rooms_and_edges(36, 36, &rooms, &edges);
        run(&mut a, 0x5EED);
        run(&mut b, 0x5EED);

        assert_eq!(a.tiles, b.tiles, "same seed must reproduce the same tile grid");

        let regions_a: Vec<_> = a
            .regions
            .iter()
            .filter(|r| r.kind == RegionKind::Corridor)
            .map(|r| (r.bounds, r.cells.clone()))
            .collect();
        let regions_b: Vec<_> = b
            .regions
            .iter()
            .filter(|r| r.kind == RegionKind::Corridor)
            .map(|r| (r.bounds, r.cells.clone()))
            .collect();
        assert_eq!(
            regions_a, regions_b,
            "same seed must reproduce the same corridor regions"
        );
    }

    /// The leg-order coin flip actually depends on the rng: at least one seed
    /// across a sample bends the elbow the other way, proving the randomness is
    /// wired in (and not a constant). Both outcomes still connect the rooms.
    #[test]
    fn leg_order_varies_with_seed() {
        let rooms = [(2, 2, 2, 2), (20, 20, 2, 2)];
        let mut seen_elbows: BTreeSet<(i32, i32)> = BTreeSet::new();
        for seed in 0..32u64 {
            let mut ctx = ctx_with_rooms_and_edges(28, 28, &rooms, &[(0, 1)]);
            run(&mut ctx, seed);
            let corridor = ctx
                .regions
                .iter()
                .find(|r| r.kind == RegionKind::Corridor)
                .unwrap();
            // The elbow is the bend: identify it as the cell present in the path
            // where the run changes axis. Simpler: record the bounds, which differ
            // only by which corner the path hugs — but bounds are identical for an
            // L either way. Instead detect the elbow via the path's turn cell.
            let c0 = rooms[0];
            let c1 = rooms[1];
            let from = Rect::new(c0.0, c0.1, c0.2, c0.3).center();
            let to = Rect::new(c1.0, c1.1, c1.2, c1.3).center();
            // Horizontal-first elbow vs vertical-first elbow.
            let hf = Point::new(to.x, from.y);
            let vf = Point::new(from.x, to.y);
            // Whichever elbow is on the carved path is the one chosen this seed.
            if corridor.cells.contains(&hf) {
                seen_elbows.insert((hf.x, hf.y));
            }
            if corridor.cells.contains(&vf) {
                seen_elbows.insert((vf.x, vf.y));
            }
        }
        assert!(
            seen_elbows.len() >= 2,
            "across seeds both L orientations should appear (saw {seen_elbows:?})"
        );
    }

    /// A graph with no edges carves nothing and adds no corridor regions.
    #[test]
    fn no_edges_carves_nothing() {
        let rooms = [(2, 2, 4, 4), (16, 16, 4, 4)];
        let mut ctx = ctx_with_rooms_and_edges(24, 24, &rooms, &[]);
        // Snapshot the room floor before; running must not change tiles.
        let before = ctx.tiles.clone();
        run(&mut ctx, 7);
        assert_eq!(ctx.tiles, before, "no edges => no carving");
        assert!(
            ctx.regions.iter().all(|r| r.kind == RegionKind::Room),
            "no edges => no corridor regions"
        );
    }

    /// This pass sets no Door tiles and leaves every edge's `at` as `None`
    /// (DoorPlacer's job, not ours).
    #[test]
    fn sets_no_doors_and_leaves_edge_at_none() {
        let rooms = [(2, 2, 4, 4), (16, 16, 4, 4)];
        let mut ctx = ctx_with_rooms_and_edges(24, 24, &rooms, &[(0, 1)]);
        run(&mut ctx, 0xBEEF);

        // No Door tile anywhere.
        assert!(
            ctx.tiles.iter().all(|(_, &t)| t != Tile::Door),
            "CorridorCarver must not place doors"
        );
        // Every edge still has at == None.
        assert!(
            ctx.graph.edges().iter().all(|e| e.at.is_none()),
            "CorridorCarver must not touch edge.at"
        );
    }
}
