//! Climate fields: temperature and moisture.
//!
//! Temperature is the cheap, legible part — a latitude band cooled by altitude.
//! Moisture is where a world stops looking like noise and starts looking like
//! geography: we sweep **prevailing wind** across the map, evaporating moisture
//! over water, raining it out over land, and dumping extra rain where the wind
//! is forced *up* a slope. The leeward side of a mountain range then falls into
//! a dry **rain shadow** — emergent deserts, not painted ones.
//!
//! Like the rest of the world generator this stays transcendental-free, so a
//! seed reproduces the same climate on every machine.

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

use super::hydrology::WaterBody;
use super::noise::Noise;
use super::{WorldConfig, Wind};

/// Computes the normalized `[0, 1]` temperature field.
///
/// Each cell starts from a triangular latitude band (1 at the equatorial middle
/// row, 0 at the poles), is cooled by `lapse_rate` for every unit of elevation
/// above sea level, and is nudged by a little fBm so the isotherms wander
/// instead of ruling dead-straight across the map.
pub fn temperature(elevation: &Grid<f32>, noise: &Noise, cfg: &WorldConfig) -> Grid<f32> {
    let (w, h) = (elevation.width(), elevation.height());
    // Avoid a divide-by-zero for a 1-row world; such a row is "the equator".
    let span = (h.max(1) - 1).max(1) as f32;
    let scale = cfg.climate_scale / w.max(1) as f32;

    Grid::from_fn(w, h, |p: Point| {
        let elev = *elevation.get(p).unwrap_or(&0.0);

        // Triangular latitude band: warmest at the middle row, coldest at the
        // top and bottom edges.
        let lat = p.y as f32 / span; // 0 at top .. 1 at bottom
        let band = 1.0 - (2.0 * lat - 1.0).abs();

        // Altitude lapse: only land above sea level cools (the sea is uniform).
        let above = (elev - cfg.sea_level).max(0.0);
        let cooled = band - cfg.lapse_rate * above;

        // Wandering isotherms.
        let jitter = cfg.temp_jitter * noise.fbm(p.x as f32 * scale, p.y as f32 * scale, 3, 2.0, 0.5);

        (cooled + jitter).clamp(0.0, 1.0)
    })
}

/// Computes the normalized `[0, 1]` moisture field via a prevailing-wind sweep.
///
/// Air enters each scan line saturated and travels along `cfg.wind`. Over water
/// it re-evaporates toward saturation; over land it rains out a fraction of what
/// it carries (`rainfall`) plus an orographic burst proportional to how steeply
/// the wind climbs (`orographic`). Carried moisture that has rained out cannot
/// come back until the wind reaches water again — that depletion is the rain
/// shadow. Land cells are finally rescaled to span `[0, 1]` so biome thresholds
/// are meaningful regardless of the absolute rainfall constants.
pub fn moisture(
    elevation: &Grid<f32>,
    water: &Grid<WaterBody>,
    noise: &Noise,
    cfg: &WorldConfig,
) -> Grid<f32> {
    let (w, h) = (elevation.width(), elevation.height());
    let mut raw = Grid::new(w, h, 0.0f32);

    let scale = cfg.climate_scale / w.max(1) as f32;
    let is_water = |p: Point| matches!(water.get(p), Some(WaterBody::Ocean | WaterBody::Lake));

    // One pass of the wind over a single scan line of cells, in travel order.
    // `line` is the ordered list of points the wind crosses.
    let sweep_line = |line: &[Point], raw: &mut Grid<f32>| {
        let mut carried = 1.0f32;
        let mut prev_elev = line.first().map(|&p| *elevation.get(p).unwrap_or(&0.0)).unwrap_or(0.0);
        for &p in line {
            let elev = *elevation.get(p).unwrap_or(&0.0);
            let m = if is_water(p) {
                carried = (carried + cfg.evaporation).min(1.0);
                1.0
            } else {
                // Rain out a *fraction* of the carried moisture: a baseline
                // drizzle plus an orographic burst that grows with the uphill
                // climb. Capping the fraction below 1 means the wind can never
                // fully desiccate in a single cell, so moisture still reaches
                // inland ranges — and the deficit it leaves behind a peak is the
                // rain shadow.
                let rise = (elev - prev_elev).max(0.0);
                let rate = (cfg.rainfall + cfg.orographic * rise).min(0.9);
                let drop = carried * rate;
                carried -= drop;
                drop
            };
            // A little texture so rainfall isn't perfectly smooth along the wind.
            let jitter = cfg.moisture_jitter * noise.fbm(p.x as f32 * scale, p.y as f32 * scale, 3, 2.0, 0.5);
            raw.set(p, m + jitter);
            prev_elev = elev;
        }
    };

    // Build each scan line in wind-travel order, then sweep it.
    match cfg.wind {
        Wind::West | Wind::East => {
            for y in 0..h as i32 {
                let mut line: Vec<Point> = (0..w as i32).map(|x| Point::new(x, y)).collect();
                if matches!(cfg.wind, Wind::East) {
                    line.reverse(); // wind from the east travels right→left
                }
                sweep_line(&line, &mut raw);
            }
        }
        Wind::North | Wind::South => {
            for x in 0..w as i32 {
                let mut line: Vec<Point> = (0..h as i32).map(|y| Point::new(x, y)).collect();
                if matches!(cfg.wind, Wind::North) {
                    line.reverse(); // wind from the north travels bottom→top
                }
                sweep_line(&line, &mut raw);
            }
        }
    }

    normalize_land(&raw, water)
}

/// Rescales the raw moisture so the **land** cells span `[0, 1]`.
///
/// Water cells get a constant 1.0 (their moisture is irrelevant — the biome
/// stage overwrites them with a water biome), and they are excluded from the
/// min/max so they can't compress the land range. A flat field (all land equal)
/// maps to a uniform 0.5 rather than dividing by zero.
fn normalize_land(raw: &Grid<f32>, water: &Grid<WaterBody>) -> Grid<f32> {
    let is_land = |p: Point| matches!(water.get(p), Some(WaterBody::Land));

    let mut lo = f32::INFINITY;
    let mut hi = f32::NEG_INFINITY;
    for (p, &v) in raw.iter() {
        if is_land(p) {
            lo = lo.min(v);
            hi = hi.max(v);
        }
    }

    let span = hi - lo;
    Grid::from_fn(raw.width(), raw.height(), |p| {
        if !is_land(p) {
            return 1.0;
        }
        if span <= 0.0 {
            0.5
        } else {
            ((*raw.get(p).unwrap_or(&lo) - lo) / span).clamp(0.0, 1.0)
        }
    })
}

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

    fn noise(seed: u64) -> Noise {
        let mut rng = Rng::from_seed(seed);
        Noise::new(&mut rng)
    }

    /// A flat sea-level world: the equator (middle row) is warmer than the poles
    /// (top/bottom rows).
    #[test]
    fn temperature_peaks_at_the_equator() {
        let cfg = WorldConfig { temp_jitter: 0.0, ..WorldConfig::default() };
        let elev = Grid::new(8, 9, cfg.sea_level); // all at sea level, no lapse
        let t = temperature(&elev, &noise(1), &cfg);

        let mid = *t.get(Point::new(4, 4)).unwrap();
        let top = *t.get(Point::new(4, 0)).unwrap();
        let bot = *t.get(Point::new(4, 8)).unwrap();
        assert!(mid > top, "equator {mid} should beat north pole {top}");
        assert!(mid > bot, "equator {mid} should beat south pole {bot}");
    }

    /// Altitude cools: a tall mountain cell is colder than a sea-level cell on
    /// the same latitude. Uses the equatorial row (y=2 of 5) so the latitude
    /// band is warm enough for the lapse rate to bite.
    #[test]
    fn altitude_cools_temperature() {
        let cfg = WorldConfig { temp_jitter: 0.0, ..WorldConfig::default() };
        let mut elev = Grid::new(5, 5, cfg.sea_level);
        elev.set(Point::new(2, 2), 0.95); // a peak on the equator
        let t = temperature(&elev, &noise(2), &cfg);
        let lowland = *t.get(Point::new(0, 2)).unwrap();
        let peak = *t.get(Point::new(2, 2)).unwrap();
        assert!(peak < lowland, "peak {peak} should be colder than lowland {lowland}");
    }

    /// A windward/leeward test: with a west wind and a single mountain wall, the
    /// windward (upwind) flank is wetter than the leeward rain shadow behind it.
    #[test]
    fn mountains_cast_a_rain_shadow() {
        let cfg = WorldConfig {
            wind: Wind::West,
            moisture_jitter: 0.0,
            ..WorldConfig::default()
        };
        let w = 12u32;
        // Ocean on the upwind (left) edge so the wind starts wet; a mountain
        // wall in the middle; dry-able lowland after it.
        let mut elev = Grid::new(w, 1, cfg.sea_level + 0.1); // lowland
        elev.set(Point::new(0, 0), 0.1); // ocean column (below sea level)
        elev.set(Point::new(5, 0), 0.9); // the mountain
        elev.set(Point::new(6, 0), 0.85);

        let mut water = Grid::new(w, 1, WaterBody::Land);
        water.set(Point::new(0, 0), WaterBody::Ocean);

        let m = moisture(&elev, &water, &noise(3), &cfg);
        let windward = *m.get(Point::new(4, 0)).unwrap(); // just upwind of peak
        let leeward = *m.get(Point::new(8, 0)).unwrap(); // behind the peak
        assert!(
            windward > leeward,
            "windward {windward} should be wetter than the rain shadow {leeward}"
        );
    }

    /// Same seed and config ⇒ identical climate fields.
    #[test]
    fn climate_is_deterministic() {
        let cfg = WorldConfig::default();
        let elev = Grid::from_fn(16, 16, |p| (p.x + p.y) as f32 / 32.0);
        let water = Grid::new(16, 16, WaterBody::Land);
        let t1 = temperature(&elev, &noise(9), &cfg);
        let t2 = temperature(&elev, &noise(9), &cfg);
        let m1 = moisture(&elev, &water, &noise(9), &cfg);
        let m2 = moisture(&elev, &water, &noise(9), &cfg);
        assert_eq!(t1, t2);
        assert_eq!(m1, m2);
    }
}
