//! The [`CaveShaper`] shaper pass: organic cellular-automata caverns.
//!
//! A *shaper* that re-sculpts some already-carved rooms into natural-looking
//! caves. It runs **after** [`RoomCarver`](crate::passes::room::RoomCarver) and
//! **before** [`MstConnect`](crate::passes::connect::MstConnect), so connectors
//! see the final cave footprint when they wire and carve corridors.
//!
//! For each qualifying room (large enough, chosen by chance) it:
//!
//! 1. Fills the room's bounding rect with random noise (interior cells start as
//!    wall with probability `fill`; a one-cell border starts as wall so the cave
//!    pulls inward), then runs `steps` rounds of the classic **4-5 cellular
//!    automaton**: a cell becomes wall when 5+ of its 8 neighbors are wall (cells
//!    off the local grid count as wall), which relaxes noise into blobby caverns.
//! 2. Keeps the **largest 4-connected floor component** — so the resulting cave
//!    is a single connected blob, never scattered pockets.
//! 3. Re-carves the room: cells in the cave become [`Floor`](crate::map::Tile::Floor),
//!    the rest of the room's old footprint reverts to [`Wall`](crate::map::Tile::Wall);
//!    the region's `cells` become the cave blob and `bounds` its bounding box.
//!
//! If the cave would be too small (or empty), the room is left as it was. Because
//! the cave is one connected component and [`CorridorCarver`](crate::passes::corridor::CorridorCarver)
//! anchors to the room cell nearest the bounds center (a cave cell), a
//! cavernized room is always reachable — connectivity is preserved.
//!
//! Caves stay [`Room`](crate::region::RegionKind::Room) regions: their organic
//! outline is what reads as a cavern, so renderers need no special case.

use serde::{Deserialize, Serialize};

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

/// Configuration for a [`CaveShaper`] pass.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct CaveConfig {
    /// Probability a qualifying room is turned into a cave.
    pub cave_chance: f64,
    /// Minimum room extent (smaller of width/height) to qualify. Caves need room
    /// to breathe; small rooms stay clean.
    pub min_room: i32,
    /// Initial wall probability for interior cells before smoothing (~0.45 gives
    /// good caves). Clamped to `[0.0, 1.0]`.
    pub fill: f64,
    /// Number of cellular-automaton smoothing rounds.
    pub steps: u32,
}

impl Default for CaveConfig {
    /// A sensible default: about a third of larger rooms (min extent >= 7) become
    /// caves, from 45%-wall noise smoothed four times.
    fn default() -> Self {
        CaveConfig {
            cave_chance: 0.30,
            min_room: 7,
            fill: 0.45,
            steps: 4,
        }
    }
}

/// Smallest cave (in cells) worth keeping; below this the room is left clean.
const MIN_CAVE_CELLS: usize = 8;

/// A cellular-automata cave shaper pass.
///
/// Construct one with [`CaveShaper::new`]; it implements [`Pass`] with the stable
/// name `"cave_shaper"`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CaveShaper {
    cfg: CaveConfig,
}

impl CaveShaper {
    /// Creates a cave shaper with the given configuration.
    pub fn new(cfg: CaveConfig) -> Self {
        CaveShaper { cfg }
    }

    /// Builds a cave blob (as global [`Point`]s) within `bounds`, or [`None`] if
    /// the smoothed automaton leaves nothing big enough to keep. Draws all
    /// randomness from `rng`.
    fn carve_cave(&self, bounds: Rect, rng: &mut Rng) -> Option<Vec<Point>> {
        let w = bounds.w.max(0) as usize;
        let h = bounds.h.max(0) as usize;
        if w < 3 || h < 3 {
            return None;
        }
        let idx = |x: usize, y: usize| y * w + x;
        let fill = self.cfg.fill.clamp(0.0, 1.0);

        // Initialize: border is wall, interior is wall with probability `fill`.
        let mut wall = vec![true; w * h];
        for y in 1..h - 1 {
            for x in 1..w - 1 {
                wall[idx(x, y)] = rng.chance(fill);
            }
        }

        // Smooth with the 4-5 rule. Cells off the grid count as wall.
        for _ in 0..self.cfg.steps {
            let mut next = wall.clone();
            for y in 0..h {
                for x in 0..w {
                    let mut walls = 0;
                    for dy in -1i32..=1 {
                        for dx in -1i32..=1 {
                            if dx == 0 && dy == 0 {
                                continue;
                            }
                            let nx = x as i32 + dx;
                            let ny = y as i32 + dy;
                            if nx < 0 || ny < 0 || nx >= w as i32 || ny >= h as i32 {
                                walls += 1; // out of bounds counts as wall
                            } else if wall[idx(nx as usize, ny as usize)] {
                                walls += 1;
                            }
                        }
                    }
                    next[idx(x, y)] = walls >= 5;
                }
            }
            wall = next;
        }

        // Keep the largest 4-connected floor component.
        let component = largest_floor_component(&wall, w, h)?;
        if component.len() < MIN_CAVE_CELLS {
            return None;
        }

        // Map local cells to global points, in row-major order.
        let mut cells: Vec<Point> = component
            .into_iter()
            .map(|i| Point::new(bounds.x + (i % w) as i32, bounds.y + (i / w) as i32))
            .collect();
        cells.sort_by_key(|p| (p.y, p.x));
        Some(cells)
    }
}

impl Pass for CaveShaper {
    fn name(&self) -> &str {
        "cave_shaper"
    }

    fn apply(&self, ctx: &mut GenContext, rng: &mut Rng) {
        for idx in 0..ctx.regions.len() {
            let region = &ctx.regions[idx];
            if region.kind != RegionKind::Room || region.cells.is_empty() {
                continue;
            }
            let bounds = region.bounds;
            if bounds.w.min(bounds.h) < self.cfg.min_room {
                continue;
            }
            if !rng.chance(self.cfg.cave_chance) {
                continue;
            }

            let Some(cave) = self.carve_cave(bounds, rng) else {
                continue;
            };

            // Re-carve: clear the room's old footprint, then carve the cave. The
            // old cells are this room's only carved area at this stage (corridors
            // run later), so clearing them touches nothing else.
            let old_cells = ctx.regions[idx].cells.clone();
            for &p in &old_cells {
                ctx.tiles.set(p, Tile::Wall);
            }
            for &p in &cave {
                ctx.tiles.set(p, Tile::Floor);
            }

            // Tighten bounds to the cave and replace the region's cells.
            let region = &mut ctx.regions[idx];
            region.bounds = bounding_rect(&cave);
            region.cells = cave;
        }
    }
}

/// Finds the largest 4-connected component of floor cells (`!wall`) in a `w`×`h`
/// grid, returned as the set of flat indices, or [`None`] if there is no floor.
fn largest_floor_component(wall: &[bool], w: usize, h: usize) -> Option<Vec<usize>> {
    let mut seen = vec![false; w * h];
    let mut best: Option<Vec<usize>> = None;
    for start in 0..w * h {
        if wall[start] || seen[start] {
            continue;
        }
        // BFS this component.
        let mut comp = Vec::new();
        let mut stack = vec![start];
        seen[start] = true;
        while let Some(i) = stack.pop() {
            comp.push(i);
            let (x, y) = (i % w, i / w);
            let mut push = |nx: usize, ny: usize| {
                let n = ny * w + nx;
                if !wall[n] && !seen[n] {
                    seen[n] = true;
                    stack.push(n);
                }
            };
            if x + 1 < w {
                push(x + 1, y);
            }
            if x > 0 {
                push(x - 1, y);
            }
            if y + 1 < h {
                push(x, y + 1);
            }
            if y > 0 {
                push(x, y - 1);
            }
        }
        if best.as_ref().map(|b| comp.len() > b.len()).unwrap_or(true) {
            best = Some(comp);
        }
    }
    best
}

/// The axis-aligned bounding box enclosing every point in `cells` (empty rect for
/// an empty slice).
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, mut max_x, mut max_y) = (first.x, first.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);
    }
    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;
    use std::collections::BTreeSet;

    fn ctx_with_room(w: u32, h: u32, room: Rect) -> GenContext {
        let mut ctx = GenContext {
            tiles: Grid::new(w, h, Tile::Wall),
            regions: Vec::new(),
            graph: ConnGraph::new(),
            blackboard: Blackboard::new(),
        };
        let cells: Vec<Point> = room.iter().collect();
        for &p in &cells {
            ctx.tiles.set(p, Tile::Floor);
        }
        ctx.add_region(RegionKind::Room, room, cells);
        ctx
    }

    fn run(ctx: &mut GenContext, cfg: CaveConfig, seed: u64) {
        let mut rng = Rng::from_seed(seed).fork("cave_shaper#0");
        CaveShaper::new(cfg).apply(ctx, &mut rng);
    }

    #[test]
    fn name_is_cave_shaper() {
        assert_eq!(CaveShaper::new(CaveConfig::default()).name(), "cave_shaper");
    }

    /// A guaranteed cave in a large room: it is non-empty, smaller than the full
    /// rect (organic), 4-connected, every cell is Floor, every cell stays inside
    /// the original room rect, and the region's cells/bounds match the cave.
    #[test]
    fn cave_is_connected_floor_within_room() {
        let room = Rect::new(0, 0, 22, 18);
        let mut ctx = ctx_with_room(22, 18, room);
        run(&mut ctx, CaveConfig { cave_chance: 1.0, min_room: 7, fill: 0.45, steps: 4 }, 0x1234);

        let r = &ctx.regions[0];
        assert!(!r.cells.is_empty(), "cave should carve cells");
        assert!((r.cells.len() as i32) < room.w * room.h, "a cave is not the full rect");

        let set: BTreeSet<(i32, i32)> = r.cells.iter().map(|p| (p.x, p.y)).collect();
        for &p in &r.cells {
            assert!(room.contains(p), "cave cell {p:?} escaped the room rect");
            assert_eq!(ctx.tiles.get(p), Some(&Tile::Floor), "cave cell {p:?} not Floor");
            assert!(r.bounds.contains(p), "cave cell {p:?} escaped bounds {:?}", r.bounds);
        }

        // 4-connected: flood from the first cell reaches all of them.
        let mut seen = BTreeSet::new();
        let mut stack = vec![(r.cells[0].x, r.cells[0].y)];
        while let Some((x, y)) = stack.pop() {
            if !set.contains(&(x, y)) || !seen.insert((x, y)) {
                continue;
            }
            stack.extend([(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)]);
        }
        assert_eq!(seen.len(), r.cells.len(), "cave must be one 4-connected blob");
    }

    /// A small room never becomes a cave; `cave_chance` 0.0 never caves anything.
    #[test]
    fn small_rooms_and_zero_chance_stay_clean() {
        let small = Rect::new(0, 0, 5, 5);
        let mut ctx = ctx_with_room(5, 5, small);
        let before = ctx.regions[0].cells.clone();
        run(&mut ctx, CaveConfig { cave_chance: 1.0, min_room: 7, fill: 0.45, steps: 4 }, 1);
        assert_eq!(ctx.regions[0].cells, before, "small room must stay a clean rect");

        let big = Rect::new(0, 0, 20, 16);
        let mut ctx2 = ctx_with_room(20, 16, big);
        let before2 = ctx2.regions[0].cells.clone();
        run(&mut ctx2, CaveConfig { cave_chance: 0.0, min_room: 7, fill: 0.45, steps: 4 }, 1);
        assert_eq!(ctx2.regions[0].cells, before2, "cave_chance 0.0 changes nothing");
    }

    /// Same seed reproduces an identical cave (tiles + region).
    #[test]
    fn caves_are_deterministic() {
        let room = Rect::new(0, 0, 20, 16);
        let cfg = CaveConfig { cave_chance: 1.0, min_room: 7, fill: 0.45, steps: 4 };
        let mut a = ctx_with_room(20, 16, room);
        let mut b = ctx_with_room(20, 16, room);
        run(&mut a, cfg, 0x5EED);
        run(&mut b, cfg, 0x5EED);
        assert_eq!(a.tiles, b.tiles);
        assert_eq!(a.regions[0].cells, b.regions[0].cells);
        assert_eq!(a.regions[0].bounds, b.regions[0].bounds);
    }
}
