//! The [`PillarPlacer`] decorator pass.
//!
//! A *decorator* is a finishing pass: it embellishes an already-carved,
//! already-connected map without changing its topology. `PillarPlacer` drops
//! free-standing [`Pillar`](crate::map::Tile::Pillar) columns into the interior
//! of larger rooms, on a regularly spaced grid, to give halls the look of a
//! pillared chamber.
//!
//! ## Connectivity is preserved by construction
//!
//! Pillars are placed as **isolated single cells** on a grid with spacing `>= 2`
//! and only in the room *interior* — never within two cells of the room's
//! bounding edge, and never on the room center. Because every pillar is a lone
//! obstacle with open floor on all four sides (the grid leaves at least one
//! floor cell between pillars), the room's floor stays fully 4-connected: you
//! can always walk around any pillar. So this pass, which runs last in the
//! dungeon recipe (after [`DoorPlacer`](crate::passes::door::DoorPlacer)), cannot
//! orphan a room — a property its tests assert directly.
//!
//! Pillars land **only on cells that are currently [`Floor`](crate::map::Tile::Floor)**
//! and that belong to a room's carved `cells`, so they respect non-rectangular
//! room shapes (a pillar never appears outside the actual carved floor) and never
//! overwrite a [`Door`](crate::map::Tile::Door) or a corridor.

use serde::{Deserialize, Serialize};

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

/// Configuration for a [`PillarPlacer`] pass.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct PillarConfig {
    /// Probability that a qualifying (large enough) room is given pillars.
    /// Clamped to `[0.0, 1.0]`; `0.0` places none.
    pub room_chance: f64,
    /// Minimum room extent (the smaller of width/height, in cells) for a room to
    /// qualify for pillars. Small rooms are left clear.
    pub min_room: i32,
    /// Grid spacing between pillars, in cells. Clamped to `>= 2` so pillars never
    /// form a solid block (which is what keeps the floor 4-connected).
    pub spacing: i32,
}

impl Default for PillarConfig {
    /// A sensible default: pillars are an *occasional* feature, not the norm —
    /// only larger rooms (min extent >= 7) qualify, and only about a third of
    /// those get a grid of pillars spaced 3 cells apart. So a typical dungeon has
    /// a couple of distinctive pillared halls — never every chamber.
    fn default() -> Self {
        PillarConfig {
            room_chance: 0.30,
            min_room: 7,
            spacing: 3,
        }
    }
}

/// A pillar-placing decorator pass.
///
/// Construct one with [`PillarPlacer::new`]; it implements [`Pass`] with the
/// stable name `"pillar_placer"`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PillarPlacer {
    cfg: PillarConfig,
}

impl PillarPlacer {
    /// Creates a pillar placer with the given configuration.
    pub fn new(cfg: PillarConfig) -> Self {
        PillarPlacer { cfg }
    }
}

impl Pass for PillarPlacer {
    fn name(&self) -> &str {
        "pillar_placer"
    }

    fn apply(&self, ctx: &mut GenContext, rng: &mut Rng) {
        let spacing = self.cfg.spacing.max(2);
        let off = spacing / 2; // phase the grid so it sits off the room edges

        // Visit rooms in id order. Collect each room's (bounds, cells) first so we
        // can mutate the grid without holding a borrow on `ctx.regions`.
        let rooms: Vec<(crate::geometry::Rect, Vec<crate::geometry::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 {
            // Only larger rooms qualify; non-qualifying rooms draw no randomness.
            if b.w.min(b.h) < self.cfg.min_room {
                continue;
            }
            // One coin per qualifying room decides whether it gets pillars.
            if !rng.chance(self.cfg.room_chance) {
                continue;
            }

            let center = b.center();
            for p in cells {
                let lx = p.x - b.x;
                let ly = p.y - b.y;
                // Keep a clear 2-cell border inside the room and skip the center.
                if lx < 2 || ly < 2 || lx > b.w - 3 || ly > b.h - 3 || p == center {
                    continue;
                }
                // Place on the spaced grid, but only on actual room floor (so the
                // pattern respects the room's shape and never hits a door).
                if (lx - off).rem_euclid(spacing) == 0
                    && (ly - off).rem_euclid(spacing) == 0
                    && ctx.tiles.get(p) == Some(&Tile::Floor)
                {
                    ctx.tiles.set(p, Tile::Pillar);
                }
            }
        }
    }
}

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

    /// Builds a context with a single rectangular room carved into Floor — the
    /// post-RoomCarver state a decorator would see, constructed by hand.
    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: PillarConfig, seed: u64) {
        let mut rng = Rng::from_seed(seed).fork("pillar_placer#0");
        PillarPlacer::new(cfg).apply(ctx, &mut rng);
    }

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

    #[test]
    fn name_is_pillar_placer() {
        assert_eq!(PillarPlacer::new(PillarConfig::default()).name(), "pillar_placer");
    }

    /// With `room_chance = 1.0` a large room gets pillars; every pillar sits on
    /// former floor, inside the 2-cell border, and not on the center.
    #[test]
    fn places_pillars_only_on_interior_floor() {
        let room = Rect::new(0, 0, 16, 12);
        let mut ctx = ctx_with_room(16, 12, room);
        run(&mut ctx, PillarConfig { room_chance: 1.0, min_room: 7, spacing: 3 }, 42);

        assert!(pillar_count(&ctx) > 0, "a 16x12 room should receive pillars");
        let center = room.center();
        for (p, &t) in ctx.tiles.iter() {
            if t == Tile::Pillar {
                let (lx, ly) = (p.x - room.x, p.y - room.y);
                assert!(lx >= 2 && ly >= 2 && lx <= room.w - 3 && ly <= room.h - 3,
                    "pillar at {p:?} is inside the clear border");
                assert_ne!(p, center, "the room center stays clear");
            }
        }
    }

    /// The room floor (Floor|Door) stays 4-connected after pillars are placed —
    /// pillars never wall off part of a room.
    #[test]
    fn pillars_keep_room_floor_connected() {
        let room = Rect::new(0, 0, 18, 14);
        let mut ctx = ctx_with_room(18, 14, room);
        run(&mut ctx, PillarConfig { room_chance: 1.0, min_room: 7, spacing: 3 }, 7);

        // Total walkable floor cells remaining.
        let floor: BTreeSet<(i32, i32)> = ctx
            .tiles
            .iter()
            .filter(|&(_, &t)| t == Tile::Floor)
            .map(|(p, _)| (p.x, p.y))
            .collect();
        assert!(!floor.is_empty());

        // Flood from any one floor cell; it must reach all of them.
        let start = *floor.iter().next().unwrap();
        let mut seen = BTreeSet::new();
        let mut stack = vec![start];
        while let Some((x, y)) = stack.pop() {
            if !floor.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(), floor.len(), "pillars must leave the floor 4-connected");
    }

    /// `room_chance = 0.0` places no pillars; a small room never qualifies.
    #[test]
    fn zero_chance_and_small_rooms_get_no_pillars() {
        let big = Rect::new(0, 0, 16, 12);
        let mut ctx = ctx_with_room(16, 12, big);
        run(&mut ctx, PillarConfig { room_chance: 0.0, min_room: 7, spacing: 3 }, 99);
        assert_eq!(pillar_count(&ctx), 0, "room_chance 0.0 places nothing");

        let small = Rect::new(0, 0, 6, 6);
        let mut ctx2 = ctx_with_room(6, 6, small);
        run(&mut ctx2, PillarConfig { room_chance: 1.0, min_room: 7, spacing: 3 }, 99);
        assert_eq!(pillar_count(&ctx2), 0, "a room below min_room gets none");
    }

    /// Same seed reproduces the same pillar layout.
    #[test]
    fn pillars_are_deterministic() {
        let room = Rect::new(0, 0, 16, 12);
        let cfg = PillarConfig::default();
        let mut a = ctx_with_room(16, 12, room);
        let mut b = ctx_with_room(16, 12, room);
        run(&mut a, cfg, 0x5EED);
        run(&mut b, cfg, 0x5EED);
        assert_eq!(a.tiles, b.tiles);
    }
}
