//! Numbers across the mapping (`extensions/scene-carriage.md` §3): WAI's Q32.32 fixed point
//! (`wai.det.fixed64`) to binary32 and binary64 and back, a rational time to binary32, and the
//! two decimal spellings the documents use (RFC 8785's for glTF's JSON, the USD Core
//! Specification's for USDA). Every conversion here is integer arithmetic or a single
//! correctly rounded cast, so it gives the same value on every machine.

/// A Q32.32 value (its raw bits) as the nearest binary32, ties to even (§3.1).
pub fn fx_to_f32(raw: i64) -> f32 {
    let m = raw.unsigned_abs();
    // `u64 as f32` rounds to nearest, ties to even; scaling by 2^-32 is then exact, since
    // every non-zero result is at least 2^-32, a normal binary32.
    let v = (m as f32) * f32::from_bits((127 - 32) << 23);
    if raw < 0 { -v } else { v }
}

/// A Q32.32 value as the nearest binary64, ties to even (§3.1).
pub fn fx_to_f64(raw: i64) -> f64 {
    let m = raw.unsigned_abs();
    let v = (m as f64) * f64::from_bits((1023 - 32) << 52);
    if raw < 0 { -v } else { v }
}

/// A binary64 as a Q32.32 value when it is one exactly: finite, a multiple of 2^-32, and
/// within the Q32.32 range. `None` otherwise (§5.2, `position_not_exact`).
pub fn f64_to_fx(v: f64) -> Option<i64> {
    if !v.is_finite() {
        return None;
    }
    if v == 0.0 {
        return Some(0);
    }
    let bits = v.to_bits();
    let neg = bits >> 63 == 1;
    let exp = ((bits >> 52) & 0x7ff) as i64;
    let frac = bits & ((1u64 << 52) - 1);
    let (mant, e) = if exp == 0 { (frac, -1074i64) } else { (frac | (1u64 << 52), exp - 1075) };
    // v = mant · 2^e; as raw Q32.32 bits: mant · 2^(e + 32).
    let s = e + 32;
    let raw: i128 = if s >= 0 {
        if s > 64 {
            return None;
        }
        (mant as i128).checked_shl(s as u32)?
    } else {
        let sh = (-s) as u32;
        if sh >= 64 || mant & ((1u64 << sh) - 1) != 0 {
            return None;
        }
        (mant >> sh) as i128
    };
    let raw = if neg { -raw } else { raw };
    i64::try_from(raw).ok()
}

/// The binary32 nearest to `num / den` (`den` > 0), ties to even (§3.1): the time of a sample.
pub fn ratio_to_f32(num: u64, den: u64) -> f32 {
    assert!(den > 0);
    if num == 0 {
        return 0.0;
    }
    // Find k with 2^23 ≤ num · 2^k / den < 2^24, then round the quotient.
    let (n, d) = (num as u128, den as u128);
    let mut k: i32 = 23 - (127 - n.leading_zeros() as i32) + (127 - d.leading_zeros() as i32);
    let scaled = |k: i32| -> (u128, u128) {
        if k >= 0 { (n << k, d) } else { (n, d << (-k)) }
    };
    loop {
        let (a, b) = scaled(k);
        let q = a / b;
        if q < (1 << 23) {
            k += 1;
        } else if q >= (1 << 24) {
            k -= 1;
        } else {
            break;
        }
    }
    let (a, b) = scaled(k);
    let (mut q, r) = (a / b, a % b);
    if 2 * r > b || (2 * r == b && q & 1 == 1) {
        q += 1;
    }
    // q · 2^-k, q < 2^24 + 1: exact in binary64, then exact in binary32.
    ((q as f64) * 2f64.powi(-k)) as f32
}

/// The shortest decimal digits and exponent of a finite non-zero binary64: `(digits, e)` with
/// the value `0.d1d2… × 10^e`'s mantissa digits and its exponent written as for
/// `d1.d2… × 10^e`.
fn shortest(v: f64) -> (String, i32) {
    let s = format!("{:e}", v.abs());
    let (m, e) = s.split_once('e').expect("exponent form");
    let digits: String = m.chars().filter(|c| *c != '.').collect();
    let digits = digits.trim_end_matches('0').to_string();
    (if digits.is_empty() { "0".into() } else { digits }, e.parse().expect("exponent"))
}

/// RFC 8785's spelling of a binary64 (ECMAScript's `Number.prototype.toString`), as glTF's
/// JSON numbers are written (§3.2).
pub fn json_number(v: f64) -> String {
    assert!(v.is_finite());
    if v == 0.0 {
        return "0".into();
    }
    let (d, e) = shortest(v);
    let sign = if v < 0.0 { "-" } else { "" };
    let k = d.len() as i32;
    let n = e + 1;
    let body = if k <= n && n <= 21 {
        format!("{d}{}", "0".repeat((n - k) as usize))
    } else if 0 < n && n <= 21 {
        format!("{}.{}", &d[..n as usize], &d[n as usize..])
    } else if -6 < n && n <= 0 {
        format!("0.{}{d}", "0".repeat((-n) as usize))
    } else {
        let rest = if k > 1 { format!(".{}", &d[1..]) } else { String::new() };
        format!("{}{rest}e{}{}", &d[..1], if n >= 1 { "+" } else { "-" }, (n - 1).abs())
    };
    format!("{sign}{body}")
}

/// The USD Core Specification's spelling of a binary64 in USDA (§3.3): the shortest digits
/// that read back to it; scientific notation when the decimal exponent is below −6 or at
/// least 15, decimal otherwise; no trailing point or zeros, no `+` in an exponent.
pub fn usd_number(v: f64) -> String {
    assert!(v.is_finite());
    if v == 0.0 {
        return "0".into();
    }
    let (d, e) = shortest(v);
    let sign = if v < 0.0 { "-" } else { "" };
    let k = d.len() as i32;
    let body = if !(-6..15).contains(&e) {
        let rest = if k > 1 { format!(".{}", &d[1..]) } else { String::new() };
        format!("{}{rest}e{e}", &d[..1])
    } else if e >= k - 1 {
        format!("{d}{}", "0".repeat((e - (k - 1)) as usize))
    } else if e >= 0 {
        format!("{}.{}", &d[..(e + 1) as usize], &d[(e + 1) as usize..])
    } else {
        format!("0.{}{d}", "0".repeat((-e - 1) as usize))
    };
    format!("{sign}{body}")
}

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

    #[test]
    fn fixed_point_converts_as_stated() {
        let one = 1i64 << 32;
        assert_eq!(fx_to_f32(one), 1.0);
        assert_eq!(fx_to_f32(-one / 2), -0.5);
        assert_eq!(fx_to_f32(1431655765), 0.333_333_34_f32); // 1/3 rounded once
        assert_eq!(fx_to_f64(1431655765), 1431655765.0 / 4294967296.0);
        assert_eq!(f64_to_fx(0.5), Some(one / 2));
        assert_eq!(f64_to_fx(-2.25), Some(-9 * one / 4));
        assert_eq!(f64_to_fx(0.1), None, "0.1 needs more than 32 fractional bits");
        assert_eq!(f64_to_fx(f64::from(0.1f32)), Some(429496736)); // 13421773 · 2^-27
        assert_eq!(f64_to_fx(2147483648.0), None, "past the Q32.32 range");
        assert_eq!(f64_to_fx(-2147483648.0), Some(i64::MIN));
        assert_eq!(f64_to_fx(1e-10), None);
    }

    #[test]
    fn times_round_once() {
        assert_eq!(ratio_to_f32(48000, 48000), 1.0);
        assert_eq!(ratio_to_f32(480, 48000), 0.01f32);
        assert_eq!(ratio_to_f32(1, 3), (1.0f64 / 3.0) as f32);
        assert_eq!(ratio_to_f32(u64::MAX, 1), 18446744073709551616.0f32);
    }

    #[test]
    fn numbers_are_spelt_as_each_format_spells_them() {
        for (v, j, u) in [
            (1.0, "1", "1"),
            (0.5, "0.5", "0.5"),
            (-2.25, "-2.25", "-2.25"),
            (1000.0, "1000", "1000"),
            (0.000001, "0.000001", "0.000001"),
            (0.0000001, "1e-7", "1e-7"),
            (1e16, "10000000000000000", "1e16"),
            (1.5e21, "1.5e+21", "1.5e21"),
            (123456789012345.0, "123456789012345", "123456789012345"),
            (1234567890123456.0, "1234567890123456", "1.234567890123456e15"),
            (f64::from(0.1f32), "0.10000000149011612", "0.10000000149011612"),
        ] {
            assert_eq!(json_number(v), j, "{v}");
            assert_eq!(usd_number(v), u, "{v}");
        }
    }
}
