//! Procedural **world** generation — continents, climate, and rivers.
//!
//! This is a sibling to the dungeon generator: where `recipes::dungeon` carves a
//! discrete `Tile` grid through a [`Pass`](crate::pass::Pass) pipeline, the world
//! generator builds *continuous fields* (elevation, temperature, moisture, water
//! flow) and classifies them into [`Biome`]s. The output is a [`World`] — a stack
//! of aligned [`Grid`]s any game or renderer can read.
//!
//! ## Stages (the recipe)
//!
//! [`generate`] runs a fixed sequence, each stage drawing from its own forked
//! RNG sub-stream (the same determinism discipline the dungeon pipeline uses):
//!
//! 1. **Elevation** — domain-warped fBm shaped into continents by an edge
//!    falloff and a redistribution curve ([`elevation`]).
//! 2. **Water bodies** — flood the sea inward; landlocked basins become lakes
//!    ([`hydrology::water_bodies`]).
//! 3. **Temperature** — latitude band cooled by altitude ([`climate::temperature`]).
//! 4. **Moisture** — prevailing-wind sweep with orographic rain and rain shadows
//!    ([`climate::moisture`]).
//! 5. **Rivers** — depression-filled flow accumulation, thresholded by density
//!    ([`hydrology::flow_accumulation`] / [`hydrology::river_cells`]).
//! 6. **Surface & biomes** — fold the above into a per-cell [`Surface`] and a
//!    [`Biome`] ([`biome::classify`]).
//!
//! ## Determinism
//!
//! Same `(config, seed)` ⇒ byte-identical [`World`] on every machine. All
//! randomness flows through [`Rng`]; the whole generator avoids transcendental
//! functions (only `+ - * /` and comparisons), so even the floating-point fields
//! reproduce exactly across platforms — the same guarantee `conventions.md` makes
//! for the dungeon core.

pub mod biome;
pub mod climate;
pub mod hydrology;
pub mod noise;

use std::error::Error;
use std::fmt;

use serde::{Deserialize, Serialize};

use crate::geometry::Point;
use crate::grid::Grid;
use crate::rng::Rng;

pub use biome::{Biome, Surface};
pub use hydrology::WaterBody;
use noise::Noise;

/// The prevailing wind direction, naming the edge the wind blows *from*.
///
/// Moisture-bearing air enters from this edge, so windward coasts on this side
/// are wet and the far (leeward) interiors fall into rain shadows.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Wind {
    /// Wind out of the west, traveling left→right.
    West,
    /// Wind out of the east, traveling right→left.
    East,
    /// Wind out of the north, traveling top→bottom.
    North,
    /// Wind out of the south, traveling bottom→top.
    South,
}

/// Every knob that shapes a [`World`]. One flat struct (rather than per-stage
/// configs) so the whole generator is configured — and later exposed over an
/// API — through a single serializable object.
///
/// [`Default`] produces a recognizable continent-and-sea world on a 256×160
/// canvas; the field docs describe what each knob does and the direction it
/// pushes.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct WorldConfig {
    /// Canvas width in cells.
    pub width: u32,
    /// Canvas height in cells.
    pub height: u32,

    // --- elevation ------------------------------------------------------
    /// Elevation below which a cell is water. Higher ⇒ more sea.
    pub sea_level: f32,
    /// Base fBm cycles across the map width. Higher ⇒ more, smaller landmasses.
    pub continent_scale: f32,
    /// Number of fBm octaves for elevation detail.
    pub elevation_octaves: u32,
    /// Frequency multiplier between octaves.
    pub lacunarity: f32,
    /// Amplitude multiplier between octaves.
    pub gain: f32,
    /// Domain-warp strength (in coordinate cycles). Higher ⇒ more sinuous coasts.
    pub warp: f32,
    /// How hard the map borders are pulled down to ocean, `0.0`..=`1.0`. Higher
    /// ⇒ the world is an island surrounded by sea; `0.0` lets land run off the
    /// edge.
    pub edge_falloff: f32,
    /// Lowland-deepening: blends elevation toward its square, `0.0`..=`1.0`.
    /// Higher ⇒ broader lowlands/seas with sharper, rarer peaks.
    pub redistribution: f32,

    // --- climate --------------------------------------------------------
    /// Cooling per unit of elevation above sea level.
    pub lapse_rate: f32,
    /// Amplitude of the fBm wobble applied to temperature isotherms.
    pub temp_jitter: f32,
    /// Prevailing wind direction (drives the moisture sweep).
    pub wind: Wind,
    /// Fraction of carried moisture rained out per land cell (baseline drizzle).
    pub rainfall: f32,
    /// Moisture re-gained toward saturation per water cell the wind crosses.
    pub evaporation: f32,
    /// Multiplier on orographic (uphill) rain — how sharply slopes wring out the
    /// wind and deepen the rain shadow behind them.
    pub orographic: f32,
    /// Amplitude of the fBm wobble applied to moisture.
    pub moisture_jitter: f32,
    /// Noise scale (cycles across width) for the temperature/moisture wobble.
    pub climate_scale: f32,

    // --- hydrology & biomes --------------------------------------------
    /// How much moisture boosts a cell's river contribution (drainage area is
    /// always counted; this adds rainfall weighting on top).
    pub river_moisture_weight: f32,
    /// Fraction of land cells that read as river, `0.0`..=`1.0`.
    pub river_density: f32,
    /// Elevation at/above which land becomes bare rock or snow (the alpine band).
    pub high_alt: f32,
    /// Ocean shallower than this depth below sea level renders as a coastal shelf.
    pub shallow_depth: f32,
    /// Land within this elevation band above sea level, and touching the ocean,
    /// is beach.
    pub beach_width: f32,
}

impl Default for WorldConfig {
    fn default() -> Self {
        WorldConfig {
            width: 256,
            height: 160,

            sea_level: 0.38,
            continent_scale: 2.5,
            elevation_octaves: 6,
            lacunarity: 2.0,
            gain: 0.5,
            warp: 0.55,
            edge_falloff: 0.82,
            redistribution: 0.35,

            lapse_rate: 0.65,
            temp_jitter: 0.06,
            wind: Wind::West,
            rainfall: 0.04,
            evaporation: 0.20,
            orographic: 3.0,
            moisture_jitter: 0.06,
            climate_scale: 3.5,

            river_moisture_weight: 0.6,
            river_density: 0.06,
            high_alt: 0.80,
            shallow_depth: 0.05,
            beach_width: 0.015,
        }
    }
}

/// Why a [`WorldConfig`] was rejected (returned instead of panicking, so a
/// caller — e.g. a future interactive editor — can surface and fix it).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum WorldConfigError {
    /// Width or height is zero — nothing to generate.
    ZeroDimension,
    /// `sea_level` is not strictly inside `(0, 1)`, so the world is all land or
    /// all sea.
    SeaLevelOutOfRange,
    /// `elevation_octaves` is zero — fBm needs at least one octave.
    NoOctaves,
}

impl fmt::Display for WorldConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            WorldConfigError::ZeroDimension => {
                write!(f, "world width and height must both be greater than zero")
            }
            WorldConfigError::SeaLevelOutOfRange => {
                write!(f, "sea_level must be strictly between 0 and 1")
            }
            WorldConfigError::NoOctaves => {
                write!(f, "elevation_octaves must be at least 1")
            }
        }
    }
}

impl Error for WorldConfigError {}

impl WorldConfig {
    /// Validates the config, returning the first problem found or `Ok`.
    pub fn validate(&self) -> Result<(), WorldConfigError> {
        if self.width == 0 || self.height == 0 {
            return Err(WorldConfigError::ZeroDimension);
        }
        if !(self.sea_level > 0.0 && self.sea_level < 1.0) {
            return Err(WorldConfigError::SeaLevelOutOfRange);
        }
        if self.elevation_octaves == 0 {
            return Err(WorldConfigError::NoOctaves);
        }
        Ok(())
    }
}

/// A generated world: a stack of aligned `width × height` field grids plus the
/// derived [`Surface`] and [`Biome`] classifications.
///
/// All grids share the same dimensions and indexing, so a cell `(x, y)` reads
/// the same location in every layer. The continuous fields are normalized to
/// `[0, 1]`; `flow` is raw drainage accumulation (unbounded, larger toward the
/// coast).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct World {
    /// Canvas width in cells.
    pub width: u32,
    /// Canvas height in cells.
    pub height: u32,
    /// The seed this world was generated from.
    pub seed: u64,
    /// The config that produced this world (carried so consumers can read the
    /// parameters — sea level, wind, etc. — that the fields were built against).
    pub config: WorldConfig,
    /// Normalized elevation, `0.0` (deepest sea) .. `1.0` (highest peak).
    pub elevation: Grid<f32>,
    /// Normalized temperature, `0.0` (polar) .. `1.0` (equatorial).
    pub temperature: Grid<f32>,
    /// Normalized moisture, `0.0` (arid) .. `1.0` (saturated).
    pub moisture: Grid<f32>,
    /// Standing-water classification (ocean/lake/land) before rivers.
    pub water: Grid<WaterBody>,
    /// Drainage accumulation — total upstream rainfall passing through a cell.
    pub flow: Grid<f32>,
    /// Per-cell surface (land/ocean/lake/river/beach) feeding biome choice.
    pub surface: Grid<Surface>,
    /// The final biome classification per cell.
    pub biome: Grid<Biome>,
}

/// Generates a [`World`] from `cfg` and `seed`, or a [`WorldConfigError`] if the
/// config is invalid.
///
/// Each stage forks its own RNG sub-stream from the seed (`"elevation"`,
/// `"temperature"`, `"moisture"`), so the noise fields are decorrelated and —
/// because [`Rng::fork`] keys only on `(seed, label)` — reordering or inserting a
/// stage never reshuffles another's randomness (spec §7 in spirit).
pub fn generate(cfg: WorldConfig, seed: u64) -> Result<World, WorldConfigError> {
    cfg.validate()?;

    let root = Rng::from_seed(seed);
    let elevation_noise = Noise::new(&mut root.fork("elevation"));
    let temperature_noise = Noise::new(&mut root.fork("temperature"));
    let moisture_noise = Noise::new(&mut root.fork("moisture"));

    // 1. Elevation → continents.
    let elevation = elevation(&cfg, &elevation_noise);

    // 2. Where the standing water sits (ocean vs landlocked lake).
    let water = hydrology::water_bodies(&elevation, &cfg);

    // 3 & 4. Climate fields.
    let temperature = climate::temperature(&elevation, &temperature_noise, &cfg);
    let moisture = climate::moisture(&elevation, &water, &moisture_noise, &cfg);

    // 5. Rivers from depression-filled flow accumulation.
    let flow = hydrology::flow_accumulation(&elevation, &moisture, &water, &cfg);
    let rivers = hydrology::river_cells(&flow, &water, cfg.river_density);

    // 6. Fold everything into surfaces and biomes.
    let surface = assemble_surface(&elevation, &water, &rivers, &cfg);
    let biome = Grid::from_fn(cfg.width, cfg.height, |p| {
        biome::classify(
            *surface.get(p).unwrap_or(&Surface::Land),
            *elevation.get(p).unwrap_or(&0.0),
            *temperature.get(p).unwrap_or(&0.5),
            *moisture.get(p).unwrap_or(&0.5),
            cfg.high_alt,
        )
    });

    Ok(World {
        width: cfg.width,
        height: cfg.height,
        seed,
        config: cfg,
        elevation,
        temperature,
        moisture,
        water,
        flow,
        surface,
        biome,
    })
}

/// Builds the normalized elevation field: domain-warped fBm, shaped into
/// continents by an edge falloff (pull the borders down to sea) and a
/// redistribution curve (deepen lowlands so coastlines and mountains read).
fn elevation(cfg: &WorldConfig, noise: &Noise) -> Grid<f32> {
    let (w, h) = (cfg.width, cfg.height);
    // Sample both axes against the *width* so features stay square (not stretched
    // when the canvas isn't). Guard the 1-px degenerate dimensions.
    let scale = cfg.continent_scale / w.max(1) as f32;
    let denom_x = (w.max(2) - 1) as f32;
    let denom_y = (h.max(2) - 1) as f32;

    Grid::from_fn(w, h, |p: Point| {
        let nx = p.x as f32 * scale;
        let ny = p.y as f32 * scale;
        let base = noise.fbm_warped(nx, ny, cfg.elevation_octaves, cfg.lacunarity, cfg.gain, cfg.warp);
        let mut e = 0.5 + 0.5 * base; // [-1,1] → [0,1]

        // Edge falloff: distance to the nearest border, 0 center → 1 edge.
        let fx = (2.0 * p.x as f32 / denom_x - 1.0).abs();
        let fy = (2.0 * p.y as f32 / denom_y - 1.0).abs();
        let edge = fx.max(fy);
        // Keep full height until FALLOFF_START, then ramp to 0 by the border.
        const FALLOFF_START: f32 = 0.55;
        let ramp = smoothstep(FALLOFF_START, 1.0, edge); // 0 inland → 1 at edge
        e *= 1.0 - cfg.edge_falloff * ramp;

        // Lowland-deepening: blend toward e² (exact, transcendental-free).
        e = lerp(e, e * e, cfg.redistribution);

        e.clamp(0.0, 1.0)
    })
}

/// Folds elevation, water bodies, and the river mask into a per-cell [`Surface`].
///
/// Ocean depth splits deep vs. shelf sea; lakes pass through; land is river where
/// the flow mask says so, beach where it is low and touches the ocean, otherwise
/// plain land.
fn assemble_surface(
    elevation: &Grid<f32>,
    water: &Grid<WaterBody>,
    rivers: &Grid<bool>,
    cfg: &WorldConfig,
) -> Grid<Surface> {
    let touches_ocean = |p: Point| {
        water
            .neighbors8(p)
            .any(|n| water.get(n) == Some(&WaterBody::Ocean))
    };

    Grid::from_fn(elevation.width(), elevation.height(), |p| {
        let elev = *elevation.get(p).unwrap_or(&0.0);
        match water.get(p) {
            Some(WaterBody::Ocean) => {
                if cfg.sea_level - elev < cfg.shallow_depth {
                    Surface::ShallowOcean
                } else {
                    Surface::DeepOcean
                }
            }
            Some(WaterBody::Lake) => Surface::Lake,
            _ => {
                if *rivers.get(p).unwrap_or(&false) {
                    Surface::River
                } else if elev - cfg.sea_level < cfg.beach_width && touches_ocean(p) {
                    Surface::Beach
                } else {
                    Surface::Land
                }
            }
        }
    })
}

/// `smoothstep(a, b, x)`: 0 below `a`, 1 above `b`, and a smooth Hermite ramp
/// between. Degenerate `a == b` returns a hard step at the threshold.
fn smoothstep(a: f32, b: f32, x: f32) -> f32 {
    if b <= a {
        return if x < a { 0.0 } else { 1.0 };
    }
    let t = ((x - a) / (b - a)).clamp(0.0, 1.0);
    t * t * (3.0 - 2.0 * t)
}

/// Linear interpolation from `a` to `b` by `t`.
fn lerp(a: f32, b: f32, t: f32) -> f32 {
    a + t * (b - a)
}

impl World {
    /// Renders the biome map as ASCII (one [`Biome::glyph`] per cell, rows
    /// separated by newlines) for quick eyeballing under `cargo test --
    /// --nocapture`. Presentation only — the renderer owns color.
    pub fn to_ascii(&self) -> String {
        let mut out = String::with_capacity((self.width as usize + 1) * self.height as usize);
        for y in 0..self.height as i32 {
            for x in 0..self.width as i32 {
                let b = self.biome.get(Point::new(x, y)).copied().unwrap_or(Biome::DeepOcean);
                out.push(b.glyph());
            }
            out.push('\n');
        }
        out
    }

    /// The fraction of cells that are land (not any water biome) — a quick
    /// land/sea ratio for sanity checks and tuning.
    pub fn land_fraction(&self) -> f32 {
        let total = (self.width as usize * self.height as usize).max(1);
        let land = self.biome.iter().filter(|(_, b)| !b.is_water()).count();
        land as f32 / total as f32
    }
}

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

    /// Same seed + config ⇒ byte-identical worlds (the determinism contract).
    #[test]
    fn same_seed_produces_identical_worlds() {
        let cfg = WorldConfig::default();
        let a = generate(cfg, 2026).unwrap();
        let b = generate(cfg, 2026).unwrap();
        assert_eq!(a, b, "same seed must reproduce a byte-identical World");
        assert_eq!(a.to_ascii(), b.to_ascii());
    }

    /// Different seeds produce different worlds.
    #[test]
    fn distinct_seeds_diverge() {
        let cfg = WorldConfig::default();
        let a = generate(cfg, 1).unwrap();
        let b = generate(cfg, 2).unwrap();
        assert_ne!(a.biome, b.biome);
    }

    /// A default world is a believable mix: it has both sea and land, the land
    /// isn't a trivial sliver, and several distinct biomes appear.
    #[test]
    fn default_world_is_a_varied_mix() {
        let cfg = WorldConfig::default();
        for seed in [1u64, 7, 42, 100, 2026] {
            let world = generate(cfg, seed).unwrap();
            let land = world.land_fraction();
            assert!(
                (0.15..0.85).contains(&land),
                "seed {seed}: land fraction {land} is not a believable mix"
            );

            let biomes: BTreeSet<Biome> = world.biome.iter().map(|(_, &b)| b).collect();
            assert!(
                biomes.len() >= 5,
                "seed {seed}: expected a varied world, saw only {} biomes",
                biomes.len()
            );
            // There is open ocean and there is dry land.
            assert!(biomes.iter().any(|b| b.is_water()), "seed {seed}: no water");
            assert!(biomes.iter().any(|b| !b.is_water()), "seed {seed}: no land");
        }
    }

    /// Rivers appear and they are made of River-surface land cells (the flow
    /// accumulation actually carved channels).
    #[test]
    fn default_world_has_rivers() {
        let cfg = WorldConfig::default();
        let mut total_river = 0usize;
        for seed in 1..=8u64 {
            let world = generate(cfg, seed).unwrap();
            total_river += world.biome.iter().filter(|(_, &b)| b == Biome::River).count();
        }
        assert!(total_river > 0, "expected some river cells across 8 seeds");
    }

    /// Invalid configs are rejected, never panic.
    #[test]
    fn invalid_configs_are_rejected() {
        let zero = WorldConfig { width: 0, ..WorldConfig::default() };
        assert_eq!(generate(zero, 0).err(), Some(WorldConfigError::ZeroDimension));

        let bad_sea = WorldConfig { sea_level: 1.5, ..WorldConfig::default() };
        assert_eq!(generate(bad_sea, 0).err(), Some(WorldConfigError::SeaLevelOutOfRange));

        let no_oct = WorldConfig { elevation_octaves: 0, ..WorldConfig::default() };
        assert_eq!(generate(no_oct, 0).err(), Some(WorldConfigError::NoOctaves));
    }

    /// Prints one world for eyeball confirmation under `--nocapture`.
    #[test]
    fn print_one_world_for_eyeballing() {
        // A small canvas so the ASCII fits a terminal.
        let cfg = WorldConfig { width: 100, height: 50, ..WorldConfig::default() };
        let world = generate(cfg, 2026).unwrap();
        println!(
            "\n--- world seed=2026 {}x{} (land {:.0}%) ---\n{}--- end world ---",
            cfg.width,
            cfg.height,
            world.land_fraction() * 100.0,
            world.to_ascii()
        );
        assert!(!world.to_ascii().is_empty());
    }
}
