//! Deterministic, reproducible RNG — **vendored** from `combat/combat-core/src/rng.rs`
//! (which in turn vendored `reikhelm-core/src/rng.rs`). Same seed → same game on
//! every machine and every run; we never touch `thread_rng`/`rand::random()`.
//!
//! The keystone is [`Rng::fork`]: it derives an *independent* child stream from the
//! parent's **basis seed** plus a label, without drawing from or mutating the parent.
//! A child is a pure function of `(basis_seed, label)` — independent of how many values
//! were pulled from the parent first. DEEPFALL keys every roll by
//! `"p{player}:r{round}:roll{n}"`, so adding a die kind or tweaking one Lineage never
//! reshuffles another player's or another round's stream. That isolation is what makes
//! "change one knob, observe the delta" hold across the balance sweeps.

use rand_chacha::ChaCha8Rng;
// `rand_core` reached through the re-export on `rand_chacha` so the two stay
// version-locked. `Rng` carries `next_u64`; `SeedableRng` carries `seed_from_u64`.
use rand_chacha::rand_core::{Rng as _, SeedableRng};

/// A deterministic random number generator backed by ChaCha8.
#[derive(Clone, Debug)]
pub struct Rng {
    /// The stable basis used for all forks: the seed this `Rng` was built from.
    basis_seed: u64,
    /// The live ChaCha8 stream that actually produces random words.
    inner: ChaCha8Rng,
}

impl Rng {
    /// Create an `Rng` from a 64-bit seed, storing the seed as the fork basis.
    pub fn from_seed(seed: u64) -> Self {
        Self {
            basis_seed: seed,
            inner: ChaCha8Rng::seed_from_u64(seed),
        }
    }

    /// Derive an independent child `Rng` from this `Rng`'s basis seed and `label`.
    /// Does not advance `self`; depends only on `(basis_seed, label)`.
    pub fn fork(&self, label: &str) -> Rng {
        Rng::from_seed(mix_seed(self.basis_seed, label))
    }

    /// Draw a `u64` of raw randomness from the live stream.
    fn next_u64(&mut self) -> u64 {
        self.inner.next_u64()
    }

    /// Integer in the **half-open** range `[lo, hi)`. `range(0, 3)` yields `0,1,2`.
    /// If `hi <= lo` the range is empty; returns `lo` rather than panicking.
    pub fn range(&mut self, lo: i32, hi: i32) -> i32 {
        if hi <= lo {
            return lo;
        }
        let span = (hi as i64 - lo as i64) as u64;
        lo + self.below(span) as i32
    }

    /// Uniform integer in `[0, span)` via rejection sampling (no modulo bias).
    fn below(&mut self, span: u64) -> u64 {
        let threshold = u64::MAX - (u64::MAX % span);
        loop {
            let x = self.next_u64();
            if x < threshold {
                return x % span;
            }
        }
    }

    /// Roll one six-sided die: a uniform value in `0..6`.
    pub fn d6(&mut self) -> usize {
        self.below(6) as usize
    }

    /// Return `true` with probability `p_bp / 10_000` (basis points), integer-only.
    pub fn chance_bp(&mut self, p_bp: i64) -> bool {
        if p_bp <= 0 {
            return false;
        }
        if p_bp >= 10_000 {
            return true;
        }
        let span = 1u128 << 64;
        let threshold = (span * p_bp as u128) / 10_000u128;
        (self.next_u64() as u128) < threshold
    }

    /// Reference to a uniformly chosen element of `items`, or `None` if empty.
    pub fn choose<'a, T>(&mut self, items: &'a [T]) -> Option<&'a T> {
        if items.is_empty() {
            return None;
        }
        let idx = self.below(items.len() as u64) as usize;
        items.get(idx)
    }

    /// Fisher–Yates shuffle in place; slices of length < 2 are left unchanged.
    pub fn shuffle<T>(&mut self, items: &mut [T]) {
        let len = items.len();
        if len < 2 {
            return;
        }
        for i in (1..len).rev() {
            let j = self.below((i + 1) as u64) as usize;
            items.swap(i, j);
        }
    }
}

/// Mix a 64-bit basis seed with a label into a fresh 64-bit seed for [`Rng::fork`].
/// Explicit fixed integer arithmetic (splitmix64 + FNV-1a) — never `DefaultHasher`.
fn mix_seed(basis: u64, label: &str) -> u64 {
    const FNV_PRIME: u64 = 0x0000_0100_0000_01B3;
    let mut state = splitmix64(basis);
    for &byte in label.as_bytes() {
        state ^= byte as u64;
        state = state.wrapping_mul(FNV_PRIME);
    }
    splitmix64(state)
}

/// One round of the splitmix64 finalizer: a fixed, portable integer avalanche.
fn splitmix64(seed: u64) -> u64 {
    let mut z = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
    z ^ (z >> 31)
}

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

    #[test]
    fn same_seed_same_stream() {
        let mut a = Rng::from_seed(7);
        let mut b = Rng::from_seed(7);
        for _ in 0..100 {
            assert_eq!(a.next_u64(), b.next_u64());
        }
    }

    #[test]
    fn fork_is_order_independent() {
        let parent = Rng::from_seed(99);
        // Draining one child must not change a sibling forked from the same parent:
        // a fork depends only on (basis_seed, label), never on the live stream.
        let mut drained = parent.fork("alpha");
        for _ in 0..1000 {
            let _ = drained.next_u64();
        }
        let mut c2a = parent.fork("beta");
        let mut c2b = parent.fork("beta");
        for _ in 0..50 {
            assert_eq!(c2a.next_u64(), c2b.next_u64());
        }
    }

    #[test]
    fn distinct_labels_diverge() {
        let parent = Rng::from_seed(1);
        let mut a = parent.fork("x#0");
        let mut b = parent.fork("x#1");
        let mut differ = false;
        for _ in 0..20 {
            if a.next_u64() != b.next_u64() {
                differ = true;
                break;
            }
        }
        assert!(differ, "distinct labels should yield distinct streams");
    }

    #[test]
    fn d6_in_range() {
        let mut r = Rng::from_seed(42);
        for _ in 0..1000 {
            let v = r.d6();
            assert!(v < 6);
        }
    }

    #[test]
    fn range_respects_bounds() {
        let mut r = Rng::from_seed(5);
        for _ in 0..1000 {
            let v = r.range(3, 7);
            assert!((3..7).contains(&v));
        }
        assert_eq!(r.range(4, 4), 4); // empty range returns lo
    }
}
