//! `reikhelm-wasm` — the browser bridge to `reikhelm-core`.
//!
//! This crate is the WebAssembly half of the core/renderer contract. It is a
//! *thin adapter*: it calls the dungeon recipe and serializes the resulting
//! [`Map`](reikhelm_core::map::Map) (plus the per-pass snapshots) into a compact
//! JSON envelope the browser renderer can read. It holds **no generation logic**
//! and reaches into **no engine internals** — exactly like `reikhelm-viz`, just
//! targeting the browser instead of a native window. If this crate ever needed
//! to know *how* a dungeon is built in order to serialize it, the architectural
//! boundary would be wrong.
//!
//! ## The envelope
//!
//! The core's own serde representation is faithful but verbose for a renderer:
//! `Tile` serializes as the strings `"Wall"`/`"Floor"`/`"Door"`, and every cell
//! is a `{ "x":.., "y":.. }` object. For the wire we instead send tiles as a
//! row-major grid of integer codes (`0=Wall, 1=Floor, 2=Door`) and points as
//! `[x, y]` pairs. That mapping is a *renderer concern*, so it lives here — the
//! core stores no codes and no colors. Timing is deliberately **not** included;
//! the page measures wall-clock around the call with `performance.now()` (and
//! `std::time::Instant` panics on `wasm32-unknown-unknown` anyway).

use serde::Serialize;
use wasm_bindgen::prelude::*;

use reikhelm_core::entity::{Entity, EntityKind};
use reikhelm_core::geometry::Point;
use reikhelm_core::grid::Grid;
use reikhelm_core::map::{Map, Tile};
use reikhelm_core::pass::Snapshot;
use reikhelm_core::recipes::dungeon::{dungeon, DungeonConfig};
use reikhelm_core::region::{Edge, Region, RegionKind, RegionTheme};

/// The integer tile code sent on the wire. Renderer concern; not a core type.
fn tile_code(tile: Tile) -> u8 {
    match tile {
        Tile::Wall => 0,
        Tile::Floor => 1,
        Tile::Door => 2,
        Tile::Pillar => 3,
        Tile::Water => 4,
        Tile::Lava => 5,
    }
}

/// The region-kind tag sent on the wire.
fn kind_str(kind: RegionKind) -> &'static str {
    match kind {
        RegionKind::Room => "Room",
        RegionKind::Corridor => "Corridor",
    }
}

/// The region-theme tag sent on the wire (snake_case; the renderer keys its
/// per-theme palette off these). Colors live in the renderer, never the core.
fn theme_str(theme: RegionTheme) -> &'static str {
    match theme {
        RegionTheme::Forge => "forge",
        RegionTheme::Cistern => "cistern",
        RegionTheme::Hall => "hall",
        RegionTheme::Throne => "throne",
        RegionTheme::Threshold => "threshold",
        RegionTheme::Vault => "vault",
        RegionTheme::Library => "library",
        RegionTheme::Den => "den",
        RegionTheme::Crypt => "crypt",
        RegionTheme::Stone => "stone",
    }
}

/// A tile grid flattened to row-major rows of integer codes.
fn tiles_to_rows(tiles: &Grid<Tile>) -> Vec<Vec<u8>> {
    let w = tiles.width() as i32;
    let h = tiles.height() as i32;
    (0..h)
        .map(|y| {
            (0..w)
                .map(|x| tile_code(tiles.get(Point::new(x, y)).copied().unwrap_or_default()))
                .collect()
        })
        .collect()
}

/// A region's bounding box on the wire.
#[derive(Serialize)]
struct RectDto {
    x: i32,
    y: i32,
    w: i32,
    h: i32,
}

/// A semantic region on the wire: id, kind tag, bounds, `[x, y]` cells, and an
/// optional theme tag (present once a themer has run — `None` for corridors,
/// unthemed rooms, and the pre-themer snapshot stages).
#[derive(Serialize)]
struct RegionDto {
    id: usize,
    kind: &'static str,
    bounds: RectDto,
    cells: Vec<[i32; 2]>,
    #[serde(skip_serializing_if = "Option::is_none")]
    theme: Option<&'static str>,
}

impl From<&Region> for RegionDto {
    fn from(r: &Region) -> Self {
        RegionDto {
            id: r.id.0,
            kind: kind_str(r.kind),
            bounds: RectDto {
                x: r.bounds.x,
                y: r.bounds.y,
                w: r.bounds.w,
                h: r.bounds.h,
            },
            cells: r.cells.iter().map(|p| [p.x, p.y]).collect(),
            theme: r.theme.map(theme_str),
        }
    }
}

/// The entity-kind tag sent on the wire.
fn entity_kind_str(kind: EntityKind) -> &'static str {
    match kind {
        EntityKind::Entrance => "Entrance",
        EntityKind::Exit => "Exit",
        EntityKind::Treasure => "Treasure",
        EntityKind::Monster => "Monster",
    }
}

/// An entity on the wire: its kind tag and the `[x, y]` cell it occupies.
#[derive(Serialize)]
struct EntityDto {
    kind: &'static str,
    at: [i32; 2],
}

impl From<&Entity> for EntityDto {
    fn from(e: &Entity) -> Self {
        EntityDto {
            kind: entity_kind_str(e.kind),
            at: [e.at.x, e.at.y],
        }
    }
}

/// A connectivity edge on the wire. `at` is the door cell once known.
#[derive(Serialize)]
struct EdgeDto {
    a: usize,
    b: usize,
    at: Option<[i32; 2]>,
}

impl From<&Edge> for EdgeDto {
    fn from(e: &Edge) -> Self {
        EdgeDto {
            a: e.a.0,
            b: e.b.0,
            at: e.at.map(|p| [p.x, p.y]),
        }
    }
}

/// One generation stage (post-pass snapshot) on the wire.
#[derive(Serialize)]
struct SnapshotDto {
    label: String,
    tiles: Vec<Vec<u8>>,
    regions: Vec<RegionDto>,
    edges: Vec<EdgeDto>,
}

impl From<&Snapshot> for SnapshotDto {
    fn from(s: &Snapshot) -> Self {
        SnapshotDto {
            label: s.label.clone(),
            tiles: tiles_to_rows(&s.tiles),
            regions: s.regions.iter().map(RegionDto::from).collect(),
            edges: s.edges.iter().map(EdgeDto::from).collect(),
        }
    }
}

/// The successful generation envelope.
#[derive(Serialize)]
struct Envelope {
    ok: bool,
    seed: u64,
    width: u32,
    height: u32,
    tiles: Vec<Vec<u8>>,
    regions: Vec<RegionDto>,
    edges: Vec<EdgeDto>,
    entities: Vec<EntityDto>,
    snapshots: Vec<SnapshotDto>,
}

/// The error envelope (bad config, parse failure). `ok: false` so the page can
/// branch on a single field.
#[derive(Serialize)]
struct ErrorEnvelope {
    ok: bool,
    error: String,
}

/// Build the error JSON. Infallible: a fixed-shape struct of owned strings.
fn error_json(msg: String) -> String {
    serde_json::to_string(&ErrorEnvelope {
        ok: false,
        error: msg,
    })
    .unwrap_or_else(|_| r#"{"ok":false,"error":"serialization failed"}"#.to_string())
}

/// Assemble the success envelope from a generated map and its snapshots.
fn build_envelope(seed: u64, map: &Map, snapshots: &[Snapshot]) -> Envelope {
    Envelope {
        ok: true,
        seed,
        width: map.width,
        height: map.height,
        tiles: tiles_to_rows(&map.tiles),
        regions: map.regions.iter().map(RegionDto::from).collect(),
        edges: map.graph.edges().iter().map(EdgeDto::from).collect(),
        entities: map.entities.iter().map(EntityDto::from).collect(),
        snapshots: snapshots.iter().map(SnapshotDto::from).collect(),
    }
}

/// The default [`DungeonConfig`] as JSON, so the page can populate its controls
/// from the real defaults rather than hardcoding them.
#[wasm_bindgen]
pub fn default_config_json() -> String {
    serde_json::to_string(&DungeonConfig::default())
        .unwrap_or_else(|_| error_json("could not serialize default config".to_string()))
}

/// Generate a dungeon for `seed` with the given JSON `config` and return the
/// envelope as a JSON string.
///
/// `seed` is a JS number (`f64`); integer seeds below 2^53 are exact. An empty
/// or whitespace `config` uses [`DungeonConfig::default`]. A malformed config or
/// one that fails recipe validation returns an `{ "ok": false, "error": ... }`
/// envelope rather than throwing — generation itself never panics by construction.
#[wasm_bindgen]
pub fn generate(seed: f64, config: &str) -> String {
    let seed = seed as u64;

    let cfg: DungeonConfig = if config.trim().is_empty() {
        DungeonConfig::default()
    } else {
        match serde_json::from_str(config) {
            Ok(c) => c,
            Err(e) => return error_json(format!("config parse error: {e}")),
        }
    };

    let pipeline = match dungeon(cfg) {
        Ok(p) => p,
        Err(e) => return error_json(format!("invalid config: {e}")),
    };

    let (map, snapshots) = pipeline.run_with_snapshots(seed);
    serde_json::to_string(&build_envelope(seed, &map, &snapshots))
        .unwrap_or_else(|_| error_json("could not serialize map".to_string()))
}
