//! The [`PoolDecorator`] pass: water and lava pools.
//!
//! A *decorator* finishing pass that floods an organic blob of
//! [`Water`](crate::map::Tile::Water) or [`Lava`](crate::map::Tile::Lava) into
//! the interior of some larger rooms — a hazard you route around, and (for lava)
//! a light source a renderer can play with.
//!
//! ## Connectivity is guarded, not assumed
//!
//! Water and lava are **not walkable**, so a careless pool could wall a room in
//! half or cover the cell a corridor connects to. This pass is conservative:
//!
//! - A pool is a single contiguous blob grown only over **interior** room floor
//!   — at least two cells from the room's bounding edge (so the perimeter ring a
//!   corridor pierces stays floor) and never on the room center (the corridor's
//!   target).
//! - After growing a candidate blob, the pass **verifies** that the room's
//!   remaining floor is still 4-connected and still contains the center; only
//!   then does it commit. Otherwise the pool is dropped for that room.
//!
//! Because every room's center stays floor and connected to its perimeter, and
//! corridors are unchanged, the whole dungeon stays connected — a property the
//! recipe's connectivity test exercises with pools enabled.
//!
//! Pools land only on cells that are currently [`Floor`](crate::map::Tile::Floor),
//! so they respect non-rectangular room shapes and never overwrite a door,
//! corridor, or pillar.

use std::collections::BTreeSet;

use serde::{Deserialize, Serialize};

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

/// Configuration for a [`PoolDecorator`] pass.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct PoolConfig {
    /// Probability that a qualifying room is flooded with a **water** pool.
    pub water_chance: f64,
    /// Probability that a qualifying room (that did not get water) is flooded
    /// with a **lava** pool. Evaluated after the water roll, so a room holds at
    /// most one pool.
    pub lava_chance: f64,
    /// Minimum room extent (smaller of width/height) for a room to qualify.
    pub min_room: i32,
}

impl Default for PoolConfig {
    /// A sensible default: water is an occasional feature of larger rooms, lava
    /// rarer still.
    fn default() -> Self {
        PoolConfig {
            water_chance: 0.20,
            lava_chance: 0.10,
            min_room: 7,
        }
    }
}

/// A water/lava pool decorator pass.
///
/// Construct one with [`PoolDecorator::new`]; it implements [`Pass`] with the
/// stable name `"pool_decorator"`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PoolDecorator {
    cfg: PoolConfig,
}

impl PoolDecorator {
    /// Creates a pool decorator with the given configuration.
    pub fn new(cfg: PoolConfig) -> Self {
        PoolDecorator { cfg }
    }
}

impl Pass for PoolDecorator {
    fn name(&self) -> &str {
        "pool_decorator"
    }

    fn apply(&self, ctx: &mut GenContext, rng: &mut Rng) {
        let rooms: Vec<(crate::geometry::Rect, Vec<Point>)> = ctx
            .regions
            .iter()
            .filter(|r| r.kind == RegionKind::Room && !r.cells.is_empty())
            .map(|r| (r.bounds, r.cells.clone()))
            .collect();

        for (b, cells) in rooms {
            if b.w.min(b.h) < self.cfg.min_room {
                continue;
            }
            // One roll per qualifying room selects water, lava, or nothing.
            let roll = rng.chance(self.cfg.water_chance);
            let tile = if roll {
                Tile::Water
            } else if rng.chance(self.cfg.lava_chance) {
                Tile::Lava
            } else {
                continue;
            };

            let center = b.center();
            // Candidate interior floor: >=2 from the bounds edge, not the center,
            // currently Floor (so we respect the room's shape and skip pillars).
            let interior: Vec<Point> = cells
                .iter()
                .copied()
                .filter(|&p| {
                    let (lx, ly) = (p.x - b.x, p.y - b.y);
                    lx >= 2
                        && ly >= 2
                        && lx <= b.w - 3
                        && ly <= b.h - 3
                        && p != center
                        && ctx.tiles.get(p) == Some(&Tile::Floor)
                })
                .collect();
            if interior.is_empty() {
                continue;
            }
            let interior_set: BTreeSet<(i32, i32)> = interior.iter().map(|p| (p.x, p.y)).collect();

            // Grow an organic blob from a random interior seed via randomized BFS.
            let area = (b.w * b.h) as usize;
            let target = (area / 6).clamp(3, 26) + rng.range(0, 5) as usize;
            let seed = *rng.choose(&interior).expect("interior is non-empty");
            let blob = grow_blob(seed, &interior_set, target, rng);

            // Commit only if the room's remaining floor stays connected with the
            // center intact — otherwise the pool would orphan part of the room.
            if !floor_stays_connected(&cells, &blob, center) {
                continue;
            }
            for &(x, y) in &blob {
                ctx.tiles.set(Point::new(x, y), tile);
            }
        }
    }
}

/// Grow a contiguous blob from `seed` over `interior` cells via randomized
/// breadth-first expansion, up to `target` cells. The randomized frontier order
/// gives an organic (non-circular) outline. Draws from `rng`.
fn grow_blob(
    seed: Point,
    interior: &BTreeSet<(i32, i32)>,
    target: usize,
    rng: &mut Rng,
) -> BTreeSet<(i32, i32)> {
    let mut blob = BTreeSet::new();
    blob.insert((seed.x, seed.y));
    let mut frontier: Vec<(i32, i32)> = neighbors4(seed.x, seed.y)
        .into_iter()
        .filter(|c| interior.contains(c))
        .collect();

    while blob.len() < target && !frontier.is_empty() {
        // Pop a random frontier cell so the blob grows unevenly.
        let i = rng.range(0, frontier.len() as i32) as usize;
        let cell = frontier.swap_remove(i);
        if !blob.insert(cell) {
            continue;
        }
        for n in neighbors4(cell.0, cell.1) {
            if interior.contains(&n) && !blob.contains(&n) {
                frontier.push(n);
            }
        }
    }
    blob
}

/// Returns true if the room's floor — its `cells` minus the pool — is non-empty,
/// still contains `center`, and is 4-connected (so the pool splits nothing off).
fn floor_stays_connected(cells: &[Point], pool: &BTreeSet<(i32, i32)>, center: Point) -> bool {
    let floor: BTreeSet<(i32, i32)> = cells
        .iter()
        .map(|p| (p.x, p.y))
        .filter(|c| !pool.contains(c))
        .collect();
    if floor.is_empty() || !floor.contains(&(center.x, center.y)) {
        return false;
    }
    let mut seen = BTreeSet::new();
    let mut stack = vec![(center.x, center.y)];
    while let Some(c) = stack.pop() {
        if !floor.contains(&c) || !seen.insert(c) {
            continue;
        }
        for n in neighbors4(c.0, c.1) {
            stack.push(n);
        }
    }
    seen.len() == floor.len()
}

fn neighbors4(x: i32, y: i32) -> [(i32, i32); 4] {
    [(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)]
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::blackboard::Blackboard;
    use crate::geometry::Rect;
    use crate::grid::Grid;
    use crate::region::ConnGraph;

    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: PoolConfig, seed: u64) {
        let mut rng = Rng::from_seed(seed).fork("pool_decorator#0");
        PoolDecorator::new(cfg).apply(ctx, &mut rng);
    }

    fn count(ctx: &GenContext, tile: Tile) -> usize {
        ctx.tiles.iter().filter(|&(_, &t)| t == tile).count()
    }

    #[test]
    fn name_is_pool_decorator() {
        assert_eq!(PoolDecorator::new(PoolConfig::default()).name(), "pool_decorator");
    }

    /// A guaranteed water pool lands only on interior floor and keeps the room's
    /// remaining floor 4-connected (water never orphans part of a room).
    #[test]
    fn water_pool_is_interior_and_keeps_floor_connected() {
        let room = Rect::new(0, 0, 18, 14);
        let mut ctx = ctx_with_room(18, 14, room);
        run(&mut ctx, PoolConfig { water_chance: 1.0, lava_chance: 0.0, min_room: 7 }, 5);

        let water: Vec<Point> = ctx
            .tiles
            .iter()
            .filter(|&(_, &t)| t == Tile::Water)
            .map(|(p, _)| p)
            .collect();
        assert!(!water.is_empty(), "a large room should get a water pool");

        let center = room.center();
        for &p in &water {
            let (lx, ly) = (p.x - room.x, p.y - room.y);
            assert!(lx >= 2 && ly >= 2 && lx <= room.w - 3 && ly <= room.h - 3, "pool {p:?} not interior");
            assert_ne!(p, center, "pool must not cover the room center");
        }

        // Remaining floor (Floor only) is 4-connected.
        let floor: BTreeSet<(i32, i32)> = ctx
            .tiles
            .iter()
            .filter(|&(_, &t)| t == Tile::Floor)
            .map(|(p, _)| (p.x, p.y))
            .collect();
        let start = *floor.iter().next().unwrap();
        let mut seen = BTreeSet::new();
        let mut stack = vec![start];
        while let Some(c) = stack.pop() {
            if !floor.contains(&c) || !seen.insert(c) {
                continue;
            }
            for n in neighbors4(c.0, c.1) {
                stack.push(n);
            }
        }
        assert_eq!(seen.len(), floor.len(), "pool must leave the floor 4-connected");
    }

    /// Zero chances place nothing; a small room never qualifies.
    #[test]
    fn zero_chance_and_small_rooms_get_no_pools() {
        let room = Rect::new(0, 0, 18, 14);
        let mut ctx = ctx_with_room(18, 14, room);
        run(&mut ctx, PoolConfig { water_chance: 0.0, lava_chance: 0.0, min_room: 7 }, 9);
        assert_eq!(count(&ctx, Tile::Water) + count(&ctx, Tile::Lava), 0);

        let small = Rect::new(0, 0, 6, 6);
        let mut ctx2 = ctx_with_room(6, 6, small);
        run(&mut ctx2, PoolConfig { water_chance: 1.0, lava_chance: 1.0, min_room: 7 }, 9);
        assert_eq!(count(&ctx2, Tile::Water) + count(&ctx2, Tile::Lava), 0);
    }

    /// Same seed reproduces the same pool.
    #[test]
    fn pools_are_deterministic() {
        let room = Rect::new(0, 0, 18, 14);
        let cfg = PoolConfig { water_chance: 1.0, lava_chance: 0.0, min_room: 7 };
        let mut a = ctx_with_room(18, 14, room);
        let mut b = ctx_with_room(18, 14, room);
        run(&mut a, cfg, 0x5EED);
        run(&mut b, cfg, 0x5EED);
        assert_eq!(a.tiles, b.tiles);
    }
}
