//! Deterministic coherent noise: seeded Perlin gradient noise, fractal Brownian
//! motion (fBm), and domain warping.
//!
//! We **own the noise** rather than pull a `noise` crate — same reasoning the
//! project applies to `range`/`shuffle` (see `conventions.md`): a third-party
//! generator's output can drift across versions, which would silently change
//! every world for a given seed. This implementation is a pure function of a
//! permutation table built from our [`Rng`], and uses only `+ - * /` and
//! comparisons — **no transcendental functions** — so a seed reproduces the same
//! field on every machine (spec §7 in spirit).
//!
//! The classic 2D Perlin construction: lattice corners each carry a pseudo-random
//! gradient (selected by a hashed permutation), the sample's offset from each
//! corner is dotted with that corner's gradient, and the four corner
//! contributions are interpolated with Perlin's quintic fade curve.

use crate::rng::Rng;

/// A seeded 2D coherent-noise sampler.
///
/// Build one with [`Noise::new`] (consuming randomness from a forked [`Rng`]),
/// then sample with [`perlin`](Noise::perlin) for a single octave or
/// [`fbm`](Noise::fbm) for layered fractal detail. Two `Noise` values built from
/// distinct RNG sub-streams are decorrelated, which is how the generator keeps
/// elevation, moisture, and warp fields independent.
pub struct Noise {
    /// The classic Perlin permutation, doubled to 512 so corner lookups
    /// (`perm[x] + y`) never need a modulo. The first 256 entries are a
    /// shuffle of `0..256`; entries `256..512` repeat them.
    perm: [u8; 512],
}

impl Noise {
    /// Builds a sampler whose gradient layout is determined by `rng`.
    ///
    /// Draws exactly one shuffle of `0..256` from `rng`, so the permutation —
    /// and therefore every sample — is a deterministic function of the stream
    /// `rng` was forked from.
    pub fn new(rng: &mut Rng) -> Self {
        let mut base: Vec<u8> = (0..=255).collect();
        rng.shuffle(&mut base);

        let mut perm = [0u8; 512];
        for i in 0..512 {
            perm[i] = base[i & 255];
        }
        Noise { perm }
    }

    /// Samples a single Perlin octave at `(x, y)`, returning roughly `[-1, 1]`.
    ///
    /// Integer lattice cells are unit-sized, so callers scale their coordinates
    /// to choose a feature size (a smaller multiplier ⇒ broader features).
    pub fn perlin(&self, x: f32, y: f32) -> f32 {
        // Lattice cell origin (wrapped into the permutation's 0..256 domain) and
        // the in-cell fractional offset.
        let xi = (fast_floor(x) & 255) as usize;
        let yi = (fast_floor(y) & 255) as usize;
        let xf = x - fast_floor(x) as f32;
        let yf = y - fast_floor(y) as f32;

        // Quintic fade for each axis.
        let u = fade(xf);
        let v = fade(yf);

        // Hash the four cell corners. The doubled `perm` makes `+1` lookups safe
        // without wrapping arithmetic.
        let p = &self.perm;
        let aa = p[p[xi] as usize + yi] as usize;
        let ab = p[p[xi] as usize + yi + 1] as usize;
        let ba = p[p[xi + 1] as usize + yi] as usize;
        let bb = p[p[xi + 1] as usize + yi + 1] as usize;

        // Dot each corner's gradient with the offset *from that corner*, then
        // bilinearly interpolate with the faded weights.
        let x1 = lerp(grad(aa, xf, yf), grad(ba, xf - 1.0, yf), u);
        let x2 = lerp(grad(ab, xf, yf - 1.0), grad(bb, xf - 1.0, yf - 1.0), u);
        lerp(x1, x2, v)
    }

    /// Samples layered fBm at `(x, y)`, normalized to `[-1, 1]`.
    ///
    /// Sums `octaves` Perlin octaves, each `lacunarity`× the previous frequency
    /// and `gain`× the previous amplitude (the standard fractal sum). The result
    /// is divided by the total amplitude so it stays in `[-1, 1]` regardless of
    /// the octave count, which keeps downstream thresholds (sea level, etc.)
    /// stable when the detail level changes.
    pub fn fbm(&self, x: f32, y: f32, octaves: u32, lacunarity: f32, gain: f32) -> f32 {
        let mut freq = 1.0;
        let mut amp = 1.0;
        let mut sum = 0.0;
        let mut norm = 0.0;
        for _ in 0..octaves {
            sum += amp * self.perlin(x * freq, y * freq);
            norm += amp;
            amp *= gain;
            freq *= lacunarity;
        }
        if norm == 0.0 {
            0.0
        } else {
            sum / norm
        }
    }

    /// fBm with **domain warping**: the sample point is displaced by an
    /// independent fBm vector before sampling, which bends otherwise-round
    /// features into organic, wandering coastlines and ridges.
    ///
    /// `warp` scales the displacement (0.0 disables it, reproducing
    /// [`fbm`](Noise::fbm)). The warp vector is sampled from the *same* field at
    /// large fixed coordinate offsets, so it is decorrelated from the base
    /// lookup without needing a second sampler.
    pub fn fbm_warped(
        &self,
        x: f32,
        y: f32,
        octaves: u32,
        lacunarity: f32,
        gain: f32,
        warp: f32,
    ) -> f32 {
        // Two decorrelated fBm samples (offset far apart) form the warp vector.
        let wx = self.fbm(x + 113.7, y + 31.4, octaves, lacunarity, gain);
        let wy = self.fbm(x + 271.9, y + 157.2, octaves, lacunarity, gain);
        self.fbm(x + warp * wx, y + warp * wy, octaves, lacunarity, gain)
    }
}

/// Perlin's quintic fade `6t⁵ − 15t⁴ + 10t³`, which has zero first and second
/// derivatives at 0 and 1 so abutting lattice cells blend without creases.
fn fade(t: f32) -> f32 {
    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
}

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

/// `floor` as an integer without calling the libm `floor` intrinsic.
///
/// For the negative case we subtract one when the truncation rounded *up*
/// toward zero. Keeping this in integer/comparison space (no `f32::floor`) is
/// part of staying transcendental-free for cross-machine determinism.
fn fast_floor(x: f32) -> i32 {
    let t = x as i32;
    if x < t as f32 {
        t - 1
    } else {
        t
    }
}

/// Maps a corner's hash to one of eight gradient directions and dots it with the
/// offset `(x, y)`.
///
/// The eight directions are the axis and diagonal unit-ish vectors; this is the
/// compact 2D gradient set (cheaper than projecting 3D gradients and visually
/// indistinguishable at fBm scale).
fn grad(hash: usize, x: f32, y: f32) -> f32 {
    match hash & 7 {
        0 => x + y,
        1 => x - y,
        2 => -x + y,
        3 => -x - y,
        4 => x,
        5 => -x,
        6 => y,
        _ => -y,
    }
}

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

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

    /// Same seed ⇒ identical permutation ⇒ identical samples at probe points.
    #[test]
    fn same_seed_reproduces_samples() {
        let a = sampler(42);
        let b = sampler(42);
        for &(x, y) in &[(0.3, 0.7), (12.5, -4.25), (100.1, 100.1), (-7.0, 3.0)] {
            assert_eq!(a.perlin(x, y), b.perlin(x, y));
            assert_eq!(
                a.fbm(x, y, 5, 2.0, 0.5),
                b.fbm(x, y, 5, 2.0, 0.5),
                "fbm must reproduce too"
            );
        }
    }

    /// Distinct seeds give distinct fields (overwhelmingly).
    #[test]
    fn distinct_seeds_diverge() {
        let a = sampler(1);
        let b = sampler(2);
        let sa: Vec<f32> = (0..32).map(|i| a.fbm(i as f32 * 0.3, 1.0, 4, 2.0, 0.5)).collect();
        let sb: Vec<f32> = (0..32).map(|i| b.fbm(i as f32 * 0.3, 1.0, 4, 2.0, 0.5)).collect();
        assert_ne!(sa, sb);
    }

    /// Perlin is exactly zero on integer lattice points: every corner offset is
    /// zero there, so every gradient dot product vanishes.
    #[test]
    fn perlin_is_zero_on_lattice() {
        let n = sampler(7);
        for y in -3..3 {
            for x in -3..3 {
                assert_eq!(n.perlin(x as f32, y as f32), 0.0, "lattice point ({x},{y})");
            }
        }
    }

    /// fBm stays within `[-1, 1]` across a sweep (the amplitude normalization
    /// holds), so sea-level thresholds downstream are meaningful.
    #[test]
    fn fbm_is_bounded() {
        let n = sampler(123);
        for i in 0..1000 {
            let x = i as f32 * 0.137;
            let y = i as f32 * 0.071;
            let v = n.fbm(x, y, 6, 2.0, 0.5);
            assert!((-1.0..=1.0).contains(&v), "fbm out of range: {v}");
        }
    }

    /// `fast_floor` matches the mathematical floor on both signs.
    #[test]
    fn fast_floor_matches_floor() {
        let cases = [(-2.0, -2), (-1.5, -2), (-0.1, -1), (0.0, 0), (0.9, 0), (3.0, 3), (3.2, 3)];
        for (x, want) in cases {
            assert_eq!(fast_floor(x), want, "floor({x})");
        }
    }
}
