//! Biome classification — the world's analogue of the dungeon's `room_themer`:
//! a pure classifier that reads finished terrain/climate fields and labels each
//! cell, changing no underlying data.
//!
//! Water biomes (ocean/lake/river) are decided by [`crate::world::hydrology`]
//! and passed in as a [`Surface`]; everything else is a Whittaker-style lookup
//! on temperature × moisture, with elevation carving out beaches and the
//! bare/snow high country on top.

use serde::{Deserialize, Serialize};

/// What kind of water (if any) occupies a cell, decided before biome
/// classification by the hydrology stage.
///
/// `Land` cells fall through to the temperature/moisture classifier; the water
/// variants short-circuit to their corresponding [`Biome`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Surface {
    /// Dry land — classify by climate.
    Land,
    /// Open sea below the deep-water cutoff.
    DeepOcean,
    /// Sea shallow enough to read as a lighter shelf.
    ShallowOcean,
    /// An inland body of standing water (a below-sea-level basin with no path to
    /// the open ocean).
    Lake,
    /// A flowing watercourse traced by the flow-accumulation stage.
    River,
    /// Dry land immediately fringing the ocean.
    Beach,
}

/// A terrain/climate classification for a single world cell.
///
/// The land biomes are ordered cold→hot and dry→wet within each temperature
/// band, mirroring the Whittaker diagram the classifier implements. Water and
/// relief biomes (the oceans, lake, river, beach, bare rock, and snow) bracket
/// the climate table.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Biome {
    /// Open sea, below the deep-water cutoff.
    DeepOcean,
    /// Coastal shelf sea.
    ShallowOcean,
    /// Inland standing water.
    Lake,
    /// A flowing river.
    River,
    /// Sandy shore fringing the ocean.
    Beach,
    /// Permanent ice/snow — the cold high country and the polar caps.
    Snow,
    /// Frozen, treeless ground.
    Tundra,
    /// Cold coniferous forest (boreal).
    Taiga,
    /// Exposed rock above the tree line but below the snow line.
    Bare,
    /// Cool-to-temperate open grassland.
    Grassland,
    /// Dry temperate scrub.
    Shrubland,
    /// Temperate broadleaf/mixed forest.
    TemperateForest,
    /// Cool, very wet temperate forest.
    TemperateRainforest,
    /// Hot, arid desert.
    Desert,
    /// Hot grassland with sparse trees.
    Savanna,
    /// Hot forest with a pronounced dry season.
    TropicalSeasonalForest,
    /// Hot, perpetually wet jungle.
    TropicalRainforest,
}

/// Classifies one cell from its surface, elevation, and climate.
///
/// `elev`, `temp`, and `moist` are all normalized to `[0, 1]`. `high_alt` is the
/// elevation above which land becomes bare rock or snow (the alpine band),
/// expressed on the same `[0, 1]` scale. The decision order is:
///
/// 1. Any non-`Land` [`Surface`] maps straight to its water/beach biome.
/// 2. Alpine land (`elev >= high_alt`) is [`Snow`](Biome::Snow) when cold, else
///    [`Bare`](Biome::Bare) rock.
/// 3. Otherwise a Whittaker lookup on `(temp, moist)`.
pub fn classify(surface: Surface, elev: f32, temp: f32, moist: f32, high_alt: f32) -> Biome {
    // 1. Water and shoreline are already decided by hydrology.
    match surface {
        Surface::DeepOcean => return Biome::DeepOcean,
        Surface::ShallowOcean => return Biome::ShallowOcean,
        Surface::Lake => return Biome::Lake,
        Surface::River => return Biome::River,
        Surface::Beach => return Biome::Beach,
        Surface::Land => {}
    }

    // 2. The alpine band: snow caps the cold peaks, bare rock the rest.
    if elev >= high_alt {
        return if temp < 0.35 { Biome::Snow } else { Biome::Bare };
    }

    // The very coldest lowlands read as snow/ice regardless of moisture (polar
    // caps), so the temperature poles always look frozen.
    if temp < 0.12 {
        return Biome::Snow;
    }

    // 3. Whittaker: temperature picks the band, moisture the member.
    if temp < 0.30 {
        // Cold.
        if moist < 0.35 {
            Biome::Tundra
        } else {
            Biome::Taiga
        }
    } else if temp < 0.65 {
        // Temperate.
        if moist < 0.20 {
            Biome::Shrubland
        } else if moist < 0.50 {
            Biome::Grassland
        } else if moist < 0.80 {
            Biome::TemperateForest
        } else {
            Biome::TemperateRainforest
        }
    } else {
        // Hot.
        if moist < 0.20 {
            Biome::Desert
        } else if moist < 0.40 {
            Biome::Savanna
        } else if moist < 0.70 {
            Biome::TropicalSeasonalForest
        } else {
            Biome::TropicalRainforest
        }
    }
}

impl Biome {
    /// `true` if this biome is any kind of water (ocean, lake, or river).
    ///
    /// Handy for renderers and for game logic that treats all water as
    /// impassable terrain.
    pub fn is_water(self) -> bool {
        matches!(
            self,
            Biome::DeepOcean | Biome::ShallowOcean | Biome::Lake | Biome::River
        )
    }

    /// A single-character glyph for ASCII previews (one stable char per biome).
    ///
    /// Used by the world's debug `to_ascii` render and tests; it is *not* a
    /// rendering policy (colors live in the renderer, never the core).
    pub fn glyph(self) -> char {
        match self {
            Biome::DeepOcean => '~',
            Biome::ShallowOcean => '-',
            Biome::Lake => 'o',
            Biome::River => '+',
            Biome::Beach => '.',
            Biome::Snow => '*',
            Biome::Tundra => ',',
            Biome::Taiga => 't',
            Biome::Bare => '^',
            Biome::Grassland => '"',
            Biome::Shrubland => ':',
            Biome::TemperateForest => 'f',
            Biome::TemperateRainforest => 'F',
            Biome::Desert => 'd',
            Biome::Savanna => 's',
            Biome::TropicalSeasonalForest => 'j',
            Biome::TropicalRainforest => 'J',
        }
    }
}

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

    /// Each water surface short-circuits to its matching biome regardless of
    /// climate.
    #[test]
    fn water_surfaces_map_directly() {
        let hot = (0.9, 0.9);
        assert_eq!(classify(Surface::DeepOcean, 0.1, hot.0, hot.1, 0.8), Biome::DeepOcean);
        assert_eq!(classify(Surface::ShallowOcean, 0.3, hot.0, hot.1, 0.8), Biome::ShallowOcean);
        assert_eq!(classify(Surface::Lake, 0.3, hot.0, hot.1, 0.8), Biome::Lake);
        assert_eq!(classify(Surface::River, 0.5, hot.0, hot.1, 0.8), Biome::River);
        assert_eq!(classify(Surface::Beach, 0.45, hot.0, hot.1, 0.8), Biome::Beach);
    }

    /// Alpine land is snow when cold and bare rock when not.
    #[test]
    fn alpine_band_is_snow_or_bare() {
        // elev above high_alt (0.8).
        assert_eq!(classify(Surface::Land, 0.95, 0.1, 0.5, 0.8), Biome::Snow);
        assert_eq!(classify(Surface::Land, 0.95, 0.9, 0.5, 0.8), Biome::Bare);
    }

    /// The Whittaker corners land where expected: cold/dry tundra, hot/dry
    /// desert, hot/wet rainforest, temperate/mid grassland.
    #[test]
    fn whittaker_corners() {
        let a = 0.4; // mid elevation, below high_alt
        assert_eq!(classify(Surface::Land, a, 0.20, 0.10, 0.8), Biome::Tundra);
        assert_eq!(classify(Surface::Land, a, 0.90, 0.05, 0.8), Biome::Desert);
        assert_eq!(classify(Surface::Land, a, 0.90, 0.95, 0.8), Biome::TropicalRainforest);
        assert_eq!(classify(Surface::Land, a, 0.50, 0.35, 0.8), Biome::Grassland);
    }

    /// The frozen-pole shortcut: extremely cold lowland is snow even when wet.
    #[test]
    fn freezing_lowland_is_snow() {
        assert_eq!(classify(Surface::Land, 0.5, 0.05, 0.9, 0.8), Biome::Snow);
    }

    /// Every biome has a distinct ASCII glyph (no two collide), so previews are
    /// unambiguous.
    #[test]
    fn glyphs_are_distinct() {
        let all = [
            Biome::DeepOcean, Biome::ShallowOcean, Biome::Lake, Biome::River, Biome::Beach,
            Biome::Snow, Biome::Tundra, Biome::Taiga, Biome::Bare, Biome::Grassland,
            Biome::Shrubland, Biome::TemperateForest, Biome::TemperateRainforest, Biome::Desert,
            Biome::Savanna, Biome::TropicalSeasonalForest, Biome::TropicalRainforest,
        ];
        let mut glyphs: Vec<char> = all.iter().map(|b| b.glyph()).collect();
        glyphs.sort_unstable();
        let unique = {
            let mut g = glyphs.clone();
            g.dedup();
            g
        };
        assert_eq!(glyphs.len(), unique.len(), "biome glyphs must be distinct");
    }
}
