//! The [`BspPartition`] partitioner pass (spec §4.7).
//!
//! Binary space partitioning recursively cuts the whole-map rectangle into
//! smaller, non-overlapping leaf rectangles. Each cut chooses a split axis and a
//! split position at random (from the pass's dedicated [`Rng`] sub-stream), and
//! recursion stops at a leaf once it is too small to split without violating the
//! minimum leaf size, or once the configured maximum recursion depth is reached.
//!
//! Per the inter-pass data contract (see [`crate::passes`]), this pass is the
//! first in the v1 dungeon recipe: it reads an empty grid and **writes** one
//! placeholder [`Region`](crate::region::Region) of kind
//! [`Room`](crate::region::RegionKind::Room) per leaf — `bounds` set to the leaf
//! rect and `cells` empty. It carves **no** tiles; the grid stays all
//! [`Wall`](crate::map::Tile::Wall). A later `RoomCarver` pass consumes these
//! placeholders, shrinks each to an actual room, and carves floor.

use serde::{Deserialize, Serialize};

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

/// Configuration for a [`BspPartition`] pass.
///
/// `min_leaf` is the smallest allowed extent (in cells) for any dimension of a
/// leaf rectangle: a cut is only made when both resulting halves keep every
/// dimension at or above this size. `max_depth` caps how many times the
/// recursion may split; `max_depth: 0` produces a single leaf — the whole map.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BspConfig {
    /// The minimum allowed extent of any leaf dimension, in cells.
    pub min_leaf: i32,
    /// The maximum recursion depth. `0` yields a single whole-map leaf.
    pub max_depth: u32,
}

impl Default for BspConfig {
    /// A sensible default for the v1 dungeon: leaves no smaller than 6 cells on
    /// a side, recursing up to 4 levels deep (up to 16 leaves).
    fn default() -> Self {
        BspConfig {
            min_leaf: 6,
            max_depth: 4,
        }
    }
}

/// A binary-space-partition partitioner pass (spec §4.7).
///
/// Splits the map into leaf rectangles and records one placeholder
/// [`Room`](crate::region::RegionKind::Room) region per leaf. Construct one with
/// [`BspPartition::new`]; it implements [`Pass`] with the stable name
/// `"bsp_partition"`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BspPartition {
    /// The split limits this pass partitions under.
    cfg: BspConfig,
}

impl BspPartition {
    /// Creates a partitioner with the given configuration.
    pub fn new(cfg: BspConfig) -> Self {
        BspPartition { cfg }
    }

    /// Recursively partitions `rect`, appending each final leaf to `leaves`.
    ///
    /// `depth` counts down from the configured maximum: recursion stops when it
    /// reaches `0` or when `rect` is too small to split (neither axis has room
    /// for two halves each at least `min_leaf` wide). Randomness — the axis
    /// choice and the split offset — is drawn from `rng` in a fixed order, so the
    /// resulting leaf set is fully determined by the seed.
    fn split(&self, rect: Rect, depth: u32, rng: &mut Rng, leaves: &mut Vec<Rect>) {
        let min = self.cfg.min_leaf;

        // A leaf must be at least `2 * min` along an axis to be cut into two
        // halves that each keep `min`. With `min` non-positive every rect is
        // "splittable", so guard against it to keep the recursion well-founded.
        let can_split_w = min > 0 && rect.w >= 2 * min;
        let can_split_h = min > 0 && rect.h >= 2 * min;

        if depth == 0 || (!can_split_w && !can_split_h) {
            leaves.push(rect);
            return;
        }

        // Choose the split axis. `true` = vertical cut (split the width into a
        // left/right pair); `false` = horizontal cut (top/bottom pair). When only
        // one axis is splittable we must take it; when both are, pick at random.
        let split_vertical = match (can_split_w, can_split_h) {
            (true, false) => true,
            (false, true) => false,
            _ => rng.chance(0.5),
        };

        let (first, second) = if split_vertical {
            // Offset is the left half's width: in `[min, w - min]` inclusive, so
            // both halves are at least `min` wide. `range` is half-open, hence
            // the `+ 1` on the upper bound.
            let offset = rng.range(min, rect.w - min + 1);
            (
                Rect::new(rect.x, rect.y, offset, rect.h),
                Rect::new(rect.x + offset, rect.y, rect.w - offset, rect.h),
            )
        } else {
            let offset = rng.range(min, rect.h - min + 1);
            (
                Rect::new(rect.x, rect.y, rect.w, offset),
                Rect::new(rect.x, rect.y + offset, rect.w, rect.h - offset),
            )
        };

        self.split(first, depth - 1, rng, leaves);
        self.split(second, depth - 1, rng, leaves);
    }
}

impl Pass for BspPartition {
    fn name(&self) -> &str {
        "bsp_partition"
    }

    fn apply(&self, ctx: &mut GenContext, rng: &mut Rng) {
        // The whole canvas is the root rectangle to partition.
        let map_rect = Rect::new(0, 0, ctx.tiles.width() as i32, ctx.tiles.height() as i32);

        let mut leaves = Vec::new();
        self.split(map_rect, self.cfg.max_depth, rng, &mut leaves);

        // One placeholder Room per leaf: bounds = leaf rect, no cells, no tiles
        // carved. `RoomCarver` refines these later.
        for leaf in leaves {
            ctx.add_region(RegionKind::Room, leaf, Vec::new());
        }
    }
}

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

    /// Builds the precondition this pass requires: a fresh `GenContext` whose
    /// grid is all `Wall` and whose regions/graph/blackboard are empty — exactly
    /// the state `Pipeline::run` would hand the first pass. Constructed by hand
    /// so the test depends on no other pass.
    fn empty_ctx(width: u32, height: u32) -> GenContext {
        GenContext {
            tiles: Grid::new(width, height, Tile::Wall),
            regions: Vec::new(),
            graph: ConnGraph::new(),
            blackboard: Blackboard::new(),
        }
    }

    /// Runs `BspPartition` with the given config/seed on a fresh `width`×`height`
    /// context and returns the resulting context for inspection.
    fn run(width: u32, height: u32, cfg: BspConfig, seed: u64) -> GenContext {
        let mut ctx = empty_ctx(width, height);
        // Mirror the pipeline's keying so we exercise the real sub-stream path.
        let mut rng = Rng::from_seed(seed).fork("bsp_partition#0");
        BspPartition::new(cfg).apply(&mut ctx, &mut rng);
        ctx
    }

    /// The pass identifies itself with the exact contract name.
    #[test]
    fn name_is_bsp_partition() {
        assert_eq!(BspPartition::new(BspConfig::default()).name(), "bsp_partition");
    }

    /// After running on an empty context, `regions` is non-empty and every
    /// region is a `Room` placeholder (kind = Room, empty `cells`).
    #[test]
    fn produces_nonempty_room_placeholders() {
        let ctx = run(64, 48, BspConfig::default(), 0xABCD);
        assert!(!ctx.regions.is_empty(), "partition must yield at least one leaf");
        assert!(
            ctx.regions.iter().all(|r| r.kind == RegionKind::Room),
            "every leaf region must be a Room"
        );
        assert!(
            ctx.regions.iter().all(|r| r.cells.is_empty()),
            "placeholders carry no cells"
        );
    }

    /// The pass carves no tiles: the grid stays entirely `Wall`.
    #[test]
    fn carves_no_tiles() {
        let mut ctx = empty_ctx(64, 48);
        let mut rng = Rng::from_seed(0xABCD).fork("bsp_partition#0");
        BspPartition::new(BspConfig::default()).apply(&mut ctx, &mut rng);
        assert!(
            ctx.tiles.iter().all(|(_, &t)| t == Tile::Wall),
            "partitioner must not carve any tile"
        );
    }

    /// Every leaf lies fully within the map and respects `min_leaf` on both
    /// dimensions.
    #[test]
    fn leaves_in_bounds_and_respect_min_leaf() {
        let cfg = BspConfig {
            min_leaf: 5,
            max_depth: 5,
        };
        let (w, h) = (80, 50);
        let map = Rect::new(0, 0, w as i32, h as i32);
        let ctx = run(w, h, cfg, 0x1234_5678);

        for r in &ctx.regions {
            let b = r.bounds;
            // Within bounds.
            assert!(
                b.x >= 0 && b.y >= 0 && b.right() <= map.right() && b.bottom() <= map.bottom(),
                "leaf {b:?} escapes the map {map:?}"
            );
            // Respects the minimum on both axes.
            assert!(
                b.w >= cfg.min_leaf && b.h >= cfg.min_leaf,
                "leaf {b:?} violates min_leaf {}",
                cfg.min_leaf
            );
        }
    }

    /// Leaves do not overlap and their areas tile the whole map exactly (no gap,
    /// no overlap): the sum of leaf areas equals the map area, and no pair of
    /// leaves intersects.
    #[test]
    fn leaves_do_not_overlap_and_cover_the_map() {
        let cfg = BspConfig {
            min_leaf: 4,
            max_depth: 6,
        };
        let (w, h) = (70, 56);
        let ctx = run(w, h, cfg, 0xDEAD_BEEF);
        let leaves: Vec<Rect> = ctx.regions.iter().map(|r| r.bounds).collect();

        // No two distinct leaves intersect.
        for (i, a) in leaves.iter().enumerate() {
            for b in &leaves[i + 1..] {
                assert!(!a.intersects(b), "leaves overlap: {a:?} and {b:?}");
            }
        }

        // Areas sum to the whole map: a partition leaves no gaps.
        let total: i64 = leaves.iter().map(|r| r.w as i64 * r.h as i64).sum();
        assert_eq!(
            total,
            w as i64 * h as i64,
            "leaf areas must tile the map exactly"
        );

        // Stronger check: every map cell is covered by exactly one leaf.
        for y in 0..h as i32 {
            for x in 0..w as i32 {
                let p = Point::new(x, y);
                let covering = leaves.iter().filter(|r| r.contains(p)).count();
                assert_eq!(covering, 1, "cell {p:?} covered by {covering} leaves");
            }
        }
    }

    /// Determinism: the same seed (same sub-stream) yields an identical leaf set
    /// (compare region bounds in order).
    #[test]
    fn same_seed_yields_identical_leaves() {
        let cfg = BspConfig::default();
        let a = run(96, 64, cfg, 0x5EED);
        let b = run(96, 64, cfg, 0x5EED);

        let bounds_a: Vec<Rect> = a.regions.iter().map(|r| r.bounds).collect();
        let bounds_b: Vec<Rect> = b.regions.iter().map(|r| r.bounds).collect();
        assert_eq!(bounds_a, bounds_b, "same seed must reproduce the same leaves");
    }

    /// Different seeds (overwhelmingly) yield a different partition.
    #[test]
    fn distinct_seeds_diverge() {
        let cfg = BspConfig::default();
        let a = run(96, 64, cfg, 1);
        let b = run(96, 64, cfg, 2);
        let bounds_a: Vec<Rect> = a.regions.iter().map(|r| r.bounds).collect();
        let bounds_b: Vec<Rect> = b.regions.iter().map(|r| r.bounds).collect();
        assert_ne!(bounds_a, bounds_b);
    }

    /// `max_depth: 0` yields a single leaf: the whole map.
    #[test]
    fn max_depth_zero_is_single_whole_map_leaf() {
        let cfg = BspConfig {
            min_leaf: 4,
            max_depth: 0,
        };
        let (w, h) = (40, 30);
        let ctx = run(w, h, cfg, 999);

        assert_eq!(ctx.regions.len(), 1, "depth 0 must produce exactly one leaf");
        assert_eq!(ctx.regions[0].kind, RegionKind::Room);
        assert_eq!(
            ctx.regions[0].bounds,
            Rect::new(0, 0, w as i32, h as i32),
            "the single leaf must be the whole map"
        );
    }

    /// A map too small to split (every extent below `2 * min_leaf`) yields the
    /// whole map as one leaf, even with depth budget to spare. Bounds-safety:
    /// no panic, no underflow.
    #[test]
    fn unsplittable_map_yields_single_leaf() {
        let cfg = BspConfig {
            min_leaf: 10,
            max_depth: 8,
        };
        // 12×12 < 2*10 on both axes, so no cut is possible.
        let ctx = run(12, 12, cfg, 42);
        assert_eq!(ctx.regions.len(), 1);
        assert_eq!(ctx.regions[0].bounds, Rect::new(0, 0, 12, 12));
    }

    /// Bounds-safety on degenerate inputs: a zero-sized map and a degenerate
    /// `min_leaf` must not panic and must still leave the grid all `Wall`.
    #[test]
    fn degenerate_inputs_do_not_panic() {
        // Zero-area map.
        let ctx = run(0, 0, BspConfig::default(), 7);
        assert_eq!(ctx.regions.len(), 1);
        assert_eq!(ctx.regions[0].bounds, Rect::new(0, 0, 0, 0));

        // min_leaf == 0: must not enable infinite recursion; treated as
        // unsplittable so a single leaf results.
        let ctx = run(
            32,
            32,
            BspConfig {
                min_leaf: 0,
                max_depth: 4,
            },
            7,
        );
        assert_eq!(ctx.regions.len(), 1);
        assert_eq!(ctx.regions[0].bounds, Rect::new(0, 0, 32, 32));
        assert!(ctx.tiles.iter().all(|(_, &t)| t == Tile::Wall));
    }
}
