//! `reikhelm-viz` — an interactive macroquad viewer for generated maps (spec §6).
//!
//! This binary is the thin renderer half of the core/viz contract: it calls a
//! recipe, receives a [`Map`](reikhelm_core::map::Map), and paints it. It holds
//! **no generation logic** and reaches into **no engine internals** — it only
//! reads the public `Map` / `Pipeline` / `Snapshot` data and turns it into
//! colored rectangles, outlines, and lines. If this file ever needed to know
//! *how* a dungeon is built in order to draw it, the architectural boundary
//! would be wrong.
//!
//! ## Controls
//! - **Space** — reroll: bump the seed, regenerate, jump to the final stage.
//! - **Left / Right** — scrub backward / forward through the generation stages
//!   (the per-pass snapshots). The final stage is the complete dungeon.
//!
//! ## How drawing works
//! Each frame we recompute a uniform cell size from the current window size and
//! the map dimensions (`min(sw/w, sh/h)`), then letterbox the grid so it stays
//! centered and square on resize. We paint the [`Snapshot`] at the current
//! stage: one filled rectangle per cell colored by its [`Tile`], plus the
//! semantic layer — region bounding-box outlines and graph edges drawn as lines
//! between the two endpoint regions' centers. Drawing the semantic layer is what
//! makes the partition and connect stages visible, since those passes change
//! only regions/edges and not tiles.

use macroquad::prelude::*;

use reikhelm_core::geometry::Rect;
use reikhelm_core::map::Tile;
use reikhelm_core::pass::Snapshot;
use reikhelm_core::recipes::dungeon::{dungeon, DungeonConfig};
use reikhelm_core::region::Edge;

/// Maps a [`Tile`] to the color it is painted with.
///
/// Purely a renderer concern — the core stores no color (spec §3). `Wall` is a
/// near-black so the carved space reads as light-on-dark; `Floor` is a warm
/// light gray; `Door` is an accent gold so thresholds pop.
fn color_of(tile: Tile) -> Color {
    match tile {
        Tile::Wall => Color::new(0.07, 0.07, 0.09, 1.0),
        Tile::Floor => Color::new(0.80, 0.76, 0.66, 1.0),
        Tile::Door => GOLD,
        Tile::Pillar => Color::new(0.30, 0.28, 0.26, 1.0),
        Tile::Water => Color::new(0.16, 0.34, 0.52, 1.0),
        Tile::Lava => Color::new(0.85, 0.32, 0.10, 1.0),
    }
}

/// How the grid is placed on screen this frame: a uniform `cell` size in pixels
/// and an `(ox, oy)` top-left offset that letterboxes (centers) the grid.
struct Layout {
    cell: f32,
    ox: f32,
    oy: f32,
}

impl Layout {
    /// Computes a centered, uniform-cell layout for a `w` × `h` grid filling the
    /// current window as large as it can without distorting the aspect ratio.
    fn fit(w: u32, h: u32) -> Layout {
        let (sw, sh) = (screen_width(), screen_height());
        // A degenerate (zero) dimension can't be drawn; fall back to a 1px cell
        // so arithmetic stays finite. The default config never produces this.
        let cell = if w == 0 || h == 0 {
            1.0
        } else {
            (sw / w as f32).min(sh / h as f32)
        };
        let ox = (sw - cell * w as f32) / 2.0;
        let oy = (sh - cell * h as f32) / 2.0;
        Layout { cell, ox, oy }
    }

    /// Pixel x of grid column `x`'s left edge.
    fn px(&self, x: i32) -> f32 {
        self.ox + x as f32 * self.cell
    }

    /// Pixel y of grid row `y`'s top edge.
    fn py(&self, y: i32) -> f32 {
        self.oy + y as f32 * self.cell
    }
}

/// A generated dungeon plus the per-pass snapshots and the seed that produced
/// them. Held in viz so reroll is a simple `seed + 1` counter — the core's
/// determinism is never touched (spec §6, §7).
struct Dungeon {
    seed: u64,
    /// One snapshot per pass, in pipeline order. The last snapshot is the
    /// completed map, so it always exists for a valid config.
    snapshots: Vec<Snapshot>,
    /// Which snapshot is currently shown (`0..snapshots.len()`).
    stage: usize,
    /// The map's dimensions, taken from the final [`Map`] (snapshots carry only
    /// the grid + semantic layer, so we keep the canvas size here).
    width: u32,
    height: u32,
}

impl Dungeon {
    /// Generates a dungeon for `seed` from the default config and starts on the
    /// final stage (the complete map).
    ///
    /// The default [`DungeonConfig`] is known-valid (its own tests assert
    /// `dungeon(DungeonConfig::default()).is_ok()`), so `expect` here can never
    /// fire; the message documents that contract for any future config change.
    fn generate(seed: u64) -> Dungeon {
        let cfg = DungeonConfig::default();
        let pipeline = dungeon(cfg).expect("the default DungeonConfig is valid by construction");
        let (map, snapshots) = pipeline.run_with_snapshots(seed);
        // Start on the final stage so the default view is the finished dungeon.
        let stage = snapshots.len().saturating_sub(1);
        Dungeon {
            seed,
            snapshots,
            stage,
            width: map.width,
            height: map.height,
        }
    }

    /// The snapshot currently being viewed, if any.
    fn current(&self) -> Option<&Snapshot> {
        self.snapshots.get(self.stage)
    }

    /// Steps the stage cursor backward (`-1`) or forward (`+1`), clamped.
    fn scrub(&mut self, delta: i32) {
        let last = self.snapshots.len().saturating_sub(1);
        let next = (self.stage as i32 + delta).clamp(0, last as i32);
        self.stage = next as usize;
    }
}

/// Draws every cell of `snapshot.tiles` as a filled rectangle colored by tile.
fn draw_tiles(snapshot: &Snapshot, layout: &Layout) {
    for (p, &tile) in snapshot.tiles.iter() {
        draw_rectangle(
            layout.px(p.x),
            layout.py(p.y),
            layout.cell,
            layout.cell,
            color_of(tile),
        );
    }
}

/// Draws each region's bounding box as an outline.
///
/// This makes the partition stage legible: `bsp_partition` fills regions while
/// the grid is still solid wall, so without outlines that stage would look
/// blank.
fn draw_region_bounds(snapshot: &Snapshot, layout: &Layout) {
    let outline = SKYBLUE;
    let thickness = (layout.cell * 0.12).max(1.0);
    for region in &snapshot.regions {
        let b: Rect = region.bounds;
        if b.is_empty() {
            continue;
        }
        draw_rectangle_lines(
            layout.px(b.x),
            layout.py(b.y),
            b.w as f32 * layout.cell,
            b.h as f32 * layout.cell,
            thickness,
            outline,
        );
    }
}

/// Draws each graph edge as a line between the centers of its two endpoint
/// regions.
///
/// `edge.a` / `edge.b` are [`RegionId`](reikhelm_core::region::RegionId)s, which
/// are indices into `snapshot.regions` (the id-as-index invariant). We resolve
/// each endpoint to its region's `bounds.center()`. This makes the connect stage
/// legible: `mst_connect` adds edges but carves no tiles.
fn draw_edges(snapshot: &Snapshot, layout: &Layout) {
    let line_color = RED;
    let thickness = (layout.cell * 0.15).max(1.0);
    // Center a line endpoint in the middle of a cell (not its corner).
    let half = layout.cell / 2.0;
    for edge in &snapshot.edges {
        let Edge { a, b, .. } = *edge;
        let (Some(ra), Some(rb)) = (snapshot.regions.get(a.0), snapshot.regions.get(b.0)) else {
            // An edge referencing a missing region would be a core bug; skip it
            // rather than panic (the renderer only reads, it never asserts).
            continue;
        };
        let ca = ra.bounds.center();
        let cb = rb.bounds.center();
        draw_line(
            layout.px(ca.x) + half,
            layout.py(ca.y) + half,
            layout.px(cb.x) + half,
            layout.py(cb.y) + half,
            thickness,
            line_color,
        );
    }
}

/// Draws the on-screen HUD: seed, current stage, and the controls hint.
///
/// When `seed_input` is `Some`, the user is typing a seed: the top line becomes
/// an entry prompt showing the buffer, and the hint switches to the entry keys.
fn draw_hud(dungeon: &Dungeon, seed_input: Option<&str>) {
    let stage_count = dungeon.snapshots.len();
    let stage_label = dungeon
        .current()
        .map(|s| s.label.as_str())
        .unwrap_or("(empty)");
    let is_final = dungeon.stage + 1 == stage_count;

    // The top line doubles as the seed-entry prompt while typing.
    let (top_line, hint) = match seed_input {
        Some(buf) => (
            format!("set seed: {buf}_"),
            "Enter: apply    Esc: cancel    (digits only)",
        ),
        None => (
            format!("seed: {}", dungeon.seed),
            "Space: reroll    Left/Right: scrub stages    S: set seed",
        ),
    };
    let stage_line = format!(
        "stage {}/{}: {}{}",
        dungeon.stage + 1,
        stage_count,
        stage_label,
        if is_final { " (final)" } else { "" },
    );

    // A dark plate behind the text so it stays readable over light floor tiles.
    draw_rectangle(0.0, 0.0, screen_width(), 64.0, Color::new(0.0, 0.0, 0.0, 0.55));
    let top_color = if seed_input.is_some() { GOLD } else { WHITE };
    draw_text(&top_line, 12.0, 22.0, 24.0, top_color);
    draw_text(&stage_line, 12.0, 44.0, 22.0, SKYBLUE);
    draw_text(hint, 12.0, 60.0, 18.0, LIGHTGRAY);
}

/// The macroquad entry point: build a dungeon, then loop forever painting it and
/// handling reroll / scrub input.
#[macroquad::main("reikhelm")]
async fn main() {
    // The seed reroll counter lives entirely in viz (spec §6/§7): the core's
    // determinism is unaffected — same seed always rebuilds the same dungeon.
    let mut seed: u64 = 0;
    let mut dungeon = Dungeon::generate(seed);
    // `Some(buffer)` while the user is typing a seed; `None` in normal mode.
    let mut seed_input: Option<String> = None;

    loop {
        // --- input ---
        if seed_input.is_none() {
            // Normal mode: reroll, scrub, or begin seed entry.
            if is_key_pressed(KeyCode::Space) {
                seed = seed.wrapping_add(1);
                dungeon = Dungeon::generate(seed);
            }
            if is_key_pressed(KeyCode::Left) {
                dungeon.scrub(-1);
            }
            if is_key_pressed(KeyCode::Right) {
                dungeon.scrub(1);
            }
            if is_key_pressed(KeyCode::S) {
                // Begin seed entry; digits are collected on the following frames.
                seed_input = Some(String::new());
            }
        } else {
            // Seed-entry mode: collect digits, apply on Enter, cancel on Esc.
            let mut exit = false;
            {
                let buf = seed_input.as_mut().expect("in seed-entry mode");
                // Drain this frame's typed chars; keep digits only (so the `S`
                // that opened the mode and any other keys are ignored). Cap the
                // length well under u64's 20 digits so it always parses.
                while let Some(c) = get_char_pressed() {
                    if c.is_ascii_digit() && buf.len() < 19 {
                        buf.push(c);
                    }
                }
                if is_key_pressed(KeyCode::Backspace) {
                    buf.pop();
                }
                if is_key_pressed(KeyCode::Enter) {
                    // Empty or unparseable buffer simply cancels (no change).
                    if let Ok(parsed) = buf.parse::<u64>() {
                        seed = parsed;
                        dungeon = Dungeon::generate(seed);
                    }
                    exit = true;
                } else if is_key_pressed(KeyCode::Escape) {
                    exit = true;
                }
            }
            if exit {
                seed_input = None;
            }
        }

        // --- draw ---
        clear_background(Color::new(0.04, 0.04, 0.05, 1.0));

        let layout = Layout::fit(dungeon.width, dungeon.height);
        if let Some(snapshot) = dungeon.current() {
            draw_tiles(snapshot, &layout);
            // The semantic overlay (region boxes + edges) is most useful while
            // scrubbing the early stages; it stays on the final view too, which
            // doubles as a quick legibility check that rooms map to regions.
            draw_region_bounds(snapshot, &layout);
            draw_edges(snapshot, &layout);
        } else {
            // No snapshots at all would mean a pass-less pipeline — never the
            // case for the dungeon recipe, but draw a note instead of a blank.
            draw_text(
                "no generation stages to display",
                20.0,
                screen_height() / 2.0,
                28.0,
                WHITE,
            );
        }

        draw_hud(&dungeon, seed_input.as_deref());

        next_frame().await;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// `color_of` is total and distinguishes every tile kind (the three v1
    /// variants paint to three distinct colors).
    #[test]
    fn color_of_is_distinct_per_tile() {
        let wall = color_of(Tile::Wall);
        let floor = color_of(Tile::Floor);
        let door = color_of(Tile::Door);
        // Compare via the f32 components; Color is not Eq (floats).
        let rgba = |c: Color| (c.r, c.g, c.b, c.a);
        assert_ne!(rgba(wall), rgba(floor));
        assert_ne!(rgba(floor), rgba(door));
        assert_ne!(rgba(wall), rgba(door));
    }
}
