//! FPSCR (Floating-Point Status and Control Register) maintenance.
//!
//! Scope per project plan: rounding modes honoured, plus the exception bits
//! games actually read (FX, FEX, VX, OX, UX, ZX, XX, FI, FPRF). Enabled-
//! exception dispatch (FE[0,1], VE/OE/UE/ZE/XE) is *not* modelled — games
//! running on Xenon almost never take FP traps.
//!
//! Bit layout (PowerISA, MSB-0 numbering; stored in a u32 with bit 31 = MSB):
//!
//! | PPC bit | u32 mask                | Name        |
//! |---------|-------------------------|-------------|
//! | 0       | `1<<31`                 | FX          |
//! | 1       | `1<<30`                 | FEX         |
//! | 2       | `1<<29`                 | VX (summary)|
//! | 3       | `1<<28`                 | OX          |
//! | 4       | `1<<27`                 | UX          |
//! | 5       | `1<<26`                 | ZX          |
//! | 6       | `1<<25`                 | XX          |
//! | 7       | `1<<24`                 | VXSNAN      |
//! | 8       | `1<<23`                 | VXISI       |
//! | 9       | `1<<22`                 | VXIDI       |
//! | 10      | `1<<21`                 | VXZDZ       |
//! | 11      | `1<<20`                 | VXIMZ       |
//! | 12      | `1<<19`                 | VXVC        |
//! | 13      | `1<<18`                 | FR          |
//! | 14      | `1<<17`                 | FI          |
//! | 15..19  | `0xF8000 >> 15` @ 15..19 | FPRF (5 bits)|
//! | 21      | `1<<10`                 | VXSOFT      |
//! | 22      | `1<<9`                  | VXSQRT      |
//! | 23      | `1<<8`                  | VXCVI       |
//! | 30..31  | `0x3`                   | RN (2 bits) |

use crate::context::PpcContext;

pub const FX: u32      = 1 << 31;
pub const FEX: u32     = 1 << 30;
pub const VX: u32      = 1 << 29;
pub const OX: u32      = 1 << 28;
pub const UX: u32      = 1 << 27;
pub const ZX: u32      = 1 << 26;
pub const XX: u32      = 1 << 25;
pub const VXSNAN: u32  = 1 << 24;
pub const VXISI: u32   = 1 << 23;
pub const VXIDI: u32   = 1 << 22;
pub const VXZDZ: u32   = 1 << 21;
pub const VXIMZ: u32   = 1 << 20;
pub const VXVC: u32    = 1 << 19;
pub const FR: u32      = 1 << 18;
pub const FI: u32      = 1 << 17;
pub const FPRF_MASK: u32 = 0x1F << 12;  // bits 15..19
pub const VXSOFT: u32  = 1 << 10;
pub const VXSQRT: u32  = 1 << 9;
pub const VXCVI: u32   = 1 << 8;
pub const RN_MASK: u32 = 0x3;

/// Union of all VX* bits (used for the VX summary recomputation).
pub const VX_ALL: u32 = VXSNAN | VXISI | VXIDI | VXZDZ | VXIMZ | VXVC | VXSOFT | VXSQRT | VXCVI;

/// FPRF classification codes (5-bit, placed in FPSCR bits 15..19).
/// The high bit ("C" in PowerISA) distinguishes ±zero/±denormal/QNaN from
/// ±normal/±inf. The next 4 bits are (FL, FG, FE, FU) = (less, greater, equal, unordered).
pub mod fprf {
    pub const QNAN: u8         = 0b1_0001;
    pub const NEG_INF: u8      = 0b0_1001;
    pub const NEG_NORMAL: u8   = 0b0_1000;
    pub const NEG_DENORMAL: u8 = 0b1_1000;
    pub const NEG_ZERO: u8     = 0b1_0010;
    pub const POS_ZERO: u8     = 0b0_0010;
    pub const POS_DENORMAL: u8 = 0b1_0100;
    pub const POS_NORMAL: u8   = 0b0_0100;
    pub const POS_INF: u8      = 0b0_0101;
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RoundingMode {
    NearestEven,    // RN=00
    TowardZero,     // RN=01
    TowardPosInf,   // RN=10
    TowardNegInf,   // RN=11
}

pub fn rounding_mode(ctx: &PpcContext) -> RoundingMode {
    match ctx.fpscr & RN_MASK {
        0 => RoundingMode::NearestEven,
        1 => RoundingMode::TowardZero,
        2 => RoundingMode::TowardPosInf,
        _ => RoundingMode::TowardNegInf,
    }
}

/// Classify a finite f64 into its FPRF 5-bit code.
pub fn classify_fprf(v: f64) -> u8 {
    if v.is_nan() {
        fprf::QNAN
    } else if v.is_infinite() {
        if v.is_sign_negative() { fprf::NEG_INF } else { fprf::POS_INF }
    } else if v == 0.0 {
        if v.is_sign_negative() { fprf::NEG_ZERO } else { fprf::POS_ZERO }
    } else if v.is_subnormal() {
        if v.is_sign_negative() { fprf::NEG_DENORMAL } else { fprf::POS_DENORMAL }
    } else if v.is_sign_negative() { fprf::NEG_NORMAL } else { fprf::POS_NORMAL }
}

/// Write FPRF into FPSCR, preserving other bits.
pub fn set_fprf(ctx: &mut PpcContext, code: u8) {
    ctx.fpscr = (ctx.fpscr & !FPRF_MASK) | ((code as u32 & 0x1F) << 12);
}

/// Set one or more exception bits on FPSCR, maintaining FX (sticky set on any
/// new exception) and VX (summary of VX* bits).
pub fn set_exception(ctx: &mut PpcContext, bits: u32) {
    let prev = ctx.fpscr;
    let new = prev | bits;
    // FX is sticky-set if any new non-sticky bit transitions to 1. PPC defines
    // FX as "any of OX, UX, ZX, XX, VX* newly set". Compute the transition set.
    let transition = (new & !prev) & (OX | UX | ZX | XX | VX_ALL);
    let mut updated = new;
    if transition != 0 {
        updated |= FX;
    }
    // Recompute VX summary from any VX* bits currently set.
    if (updated & VX_ALL) != 0 { updated |= VX; }
    ctx.fpscr = updated;
}

/// Classify the inputs of a floating-point arithmetic op and set appropriate
/// VX* bits. Returns true if any invalid-operation was detected (caller may
/// want to write a default QNaN result).
///
/// Detected cases:
///   * any SNaN input → VXSNAN
///   * infinity - infinity (same sign) → VXISI
///   * 0 / 0 → VXZDZ
///   * infinity / infinity → VXIDI
///   * 0 * infinity → VXIMZ
pub fn check_invalid_add(ctx: &mut PpcContext, a: f64, b: f64, sub: bool) -> bool {
    let mut bits = 0u32;
    if is_snan(a) || is_snan(b) { bits |= VXSNAN; }
    if a.is_infinite() && b.is_infinite() {
        // For add: VXISI iff same-sign(a,b) negated — inf - inf
        // For sub: VXISI iff same-sign(a,b) — (+inf) - (+inf) or (-inf) - (-inf)
        let both_pos = a.is_sign_positive() && b.is_sign_positive();
        let both_neg = a.is_sign_negative() && b.is_sign_negative();
        if sub {
            if both_pos || both_neg { bits |= VXISI; }
        } else {
            // add: opposite signs cancel to inf-inf
            if a.is_sign_positive() != b.is_sign_positive() { bits |= VXISI; }
        }
    }
    if bits != 0 { set_exception(ctx, bits); return true; }
    false
}

/// FMA-aware add/sub VXISI check. Per PPCBUG-202+203: the previous code
/// passed `a*c` as `lhs` to `check_invalid_add`, which suffers from two
/// rounding errors and can spuriously raise/miss VXISI in extreme cases.
/// This helper derives the mathematical product's sign and infinity status
/// from the inputs directly.
///
/// `sub` follows the same semantics as `check_invalid_add`:
///   - false (add): VXISI when product and b have opposite signs at infinity
///   - true  (sub): VXISI when product and b have same sign at infinity
pub fn check_invalid_fma_add(ctx: &mut PpcContext, a: f64, c: f64, b: f64, sub: bool) -> bool {
    let mut bits = 0u32;
    if is_snan(a) || is_snan(c) || is_snan(b) { bits |= VXSNAN; }
    let product_is_inf = (a.is_infinite() || c.is_infinite())
        && a != 0.0 && c != 0.0
        && !a.is_nan() && !c.is_nan();
    if product_is_inf && b.is_infinite() {
        let p_neg = a.is_sign_negative() != c.is_sign_negative();
        let b_neg = b.is_sign_negative();
        let same_sign = p_neg == b_neg;
        if (sub && same_sign) || (!sub && !same_sign) {
            bits |= VXISI;
        }
    }
    if bits != 0 { set_exception(ctx, bits); return true; }
    false
}

pub fn check_invalid_mul(ctx: &mut PpcContext, a: f64, b: f64) -> bool {
    let mut bits = 0u32;
    if is_snan(a) || is_snan(b) { bits |= VXSNAN; }
    let zero_times_inf =
        (a == 0.0 && b.is_infinite()) || (b == 0.0 && a.is_infinite());
    if zero_times_inf { bits |= VXIMZ; }
    if bits != 0 { set_exception(ctx, bits); return true; }
    false
}

pub fn check_invalid_div(ctx: &mut PpcContext, a: f64, b: f64) -> bool {
    let mut bits = 0u32;
    if is_snan(a) || is_snan(b) { bits |= VXSNAN; }
    if a == 0.0 && b == 0.0 { bits |= VXZDZ; }
    if a.is_infinite() && b.is_infinite() { bits |= VXIDI; }
    if bits != 0 { set_exception(ctx, bits); return true; }
    false
}

/// Divide-by-zero (finite nonzero / 0) — sets ZX but not VX.
pub fn check_zero_divide(ctx: &mut PpcContext, a: f64, b: f64) {
    if b == 0.0 && a != 0.0 && !a.is_nan() && !a.is_infinite() {
        set_exception(ctx, ZX);
    }
}

/// Post-op: classify the result and update FPRF + detect overflow/underflow/inexact.
/// `inputs_finite` lets us suppress OX for ops whose output is infinite because
/// an input already was.
pub fn update_after_op(ctx: &mut PpcContext, result: f64, inputs_were_finite: bool) {
    let mut bits = 0u32;
    if result.is_infinite() && inputs_were_finite {
        bits |= OX;
    }
    if result.is_subnormal() {
        bits |= UX;
    }
    if bits != 0 { set_exception(ctx, bits); }
    set_fprf(ctx, classify_fprf(result));
}

/// Test whether an f64 is a signalling NaN.
/// In IEEE 754-2008 (binary64), the signalling bit is the high bit of the
/// mantissa. SNaN has it clear, QNaN has it set. NaN with high mantissa bit
/// clear (and mantissa nonzero) is an SNaN.
pub fn is_snan(x: f64) -> bool {
    if !x.is_nan() { return false; }
    let bits = x.to_bits();
    // Highest mantissa bit (bit 51) clear ⇒ SNaN. Mantissa nonzero always true for NaN.
    (bits & (1u64 << 51)) == 0
}

/// Round an f64 to f32 honouring FPSCR[RN]. Uses the current hardware
/// rounding mode when RN=0 (nearest-even, the PPC default), otherwise
/// emulates the directed rounding via bit-manipulation.
pub fn round_to_single(ctx: &PpcContext, v: f64) -> f64 {
    match rounding_mode(ctx) {
        RoundingMode::NearestEven => (v as f32) as f64,
        RoundingMode::TowardZero => round_single_toward_zero(v) as f64,
        RoundingMode::TowardPosInf => round_single_toward_pos_inf(v) as f64,
        RoundingMode::TowardNegInf => round_single_toward_neg_inf(v) as f64,
    }
}

/// Round an f64 to an i64 integer honouring FPSCR[RN]. Used by fctidx.
pub fn round_to_i64(ctx: &PpcContext, v: f64) -> i64 {
    match rounding_mode(ctx) {
        RoundingMode::NearestEven => {
            // PPCBUG-221: round-half-to-even (banker's rounding). The previous
            // tie-detection used `(diff - 0.5).abs() < f64::EPSILON` which
            // breaks for |v| > 2^52 (where v.trunc() == v exactly, giving diff
            // == 0). Use a fractional-part-only check that's exact for
            // |v| <= 2^52 and degenerates correctly above.
            let t = v.trunc();
            let frac = v - t;
            let fa = frac.abs();
            if fa > 0.5 {
                t as i64 + if v >= 0.0 { 1 } else { -1 }
            } else if fa < 0.5 {
                t as i64
            } else {
                // Exact 0.5 tie — round to even.
                let fi = t as i64;
                if fi & 1 == 0 { fi } else { fi + if v >= 0.0 { 1 } else { -1 } }
            }
        }
        RoundingMode::TowardZero => v.trunc() as i64,
        RoundingMode::TowardPosInf => v.ceil() as i64,
        RoundingMode::TowardNegInf => v.floor() as i64,
    }
}

/// Round an f64 to an i32 integer honouring FPSCR[RN]. Used by fctiwx.
pub fn round_to_i32(ctx: &PpcContext, v: f64) -> i32 {
    round_to_i64(ctx, v).clamp(i32::MIN as i64, i32::MAX as i64) as i32
}

// ------ directed rounding helpers (f64 → f32) ------

fn round_single_toward_zero(v: f64) -> f32 {
    // Default f64→f32 is round-to-nearest-even. Emulate truncation:
    // take the default rounded value; if the absolute rounded magnitude
    // exceeds |v|, bump down by one ULP toward zero.
    let rn = v as f32;
    if rn.is_nan() || rn.is_infinite() || rn == 0.0 { return rn; }
    if rn.abs() as f64 <= v.abs() { return rn; }
    let adj_bits = rn.to_bits();
    // Both positive and negative finite f32 values have the IEEE-754 sign
    // bit as the MSB; subtracting 1 from `to_bits()` always reduces the
    // magnitude by one ULP (clearing the lowest mantissa bit, with carry
    // never reaching the sign bit since adj_bits is already not-zero,
    // not-inf, not-NaN, and we already returned early for those).
    let lower = adj_bits - 1;
    f32::from_bits(lower)
}

fn round_single_toward_pos_inf(v: f64) -> f32 {
    let rn = v as f32;
    if rn.is_nan() || rn.is_infinite() { return rn; }
    if (rn as f64) >= v { return rn; }
    // rn < v — bump up by one ULP in the +direction.
    let b = rn.to_bits();
    let nb = if rn.is_sign_negative() { b - 1 } else { b + 1 };
    f32::from_bits(nb)
}

fn round_single_toward_neg_inf(v: f64) -> f32 {
    let rn = v as f32;
    if rn.is_nan() || rn.is_infinite() { return rn; }
    if (rn as f64) <= v { return rn; }
    // rn > v — bump down.
    let b = rn.to_bits();
    let nb = if rn.is_sign_negative() { b + 1 } else { b - 1 };
    f32::from_bits(nb)
}

/// Drop-in replacement for the old `update_cr1_from_fpscr`. Reads the
/// currently-maintained FPSCR bits (FX, FEX, VX, OX) into CR1.
pub fn update_cr1(ctx: &mut PpcContext) {
    ctx.cr[1].lt = (ctx.fpscr & FX) != 0;
    ctx.cr[1].gt = (ctx.fpscr & FEX) != 0;
    ctx.cr[1].eq = (ctx.fpscr & VX) != 0;
    ctx.cr[1].so = (ctx.fpscr & OX) != 0;
}

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

    fn ctx() -> PpcContext { PpcContext::new() }

    #[test]
    fn rn_default_is_nearest() {
        assert_eq!(rounding_mode(&ctx()), RoundingMode::NearestEven);
    }

    #[test]
    fn rn_bits_decode() {
        let mut c = ctx();
        c.fpscr = 0x1;
        assert_eq!(rounding_mode(&c), RoundingMode::TowardZero);
        c.fpscr = 0x2;
        assert_eq!(rounding_mode(&c), RoundingMode::TowardPosInf);
        c.fpscr = 0x3;
        assert_eq!(rounding_mode(&c), RoundingMode::TowardNegInf);
    }

    #[test]
    fn fprf_classifies_correctly() {
        assert_eq!(classify_fprf(1.0), fprf::POS_NORMAL);
        assert_eq!(classify_fprf(-1.0), fprf::NEG_NORMAL);
        assert_eq!(classify_fprf(0.0), fprf::POS_ZERO);
        assert_eq!(classify_fprf(-0.0), fprf::NEG_ZERO);
        assert_eq!(classify_fprf(f64::INFINITY), fprf::POS_INF);
        assert_eq!(classify_fprf(f64::NEG_INFINITY), fprf::NEG_INF);
        assert_eq!(classify_fprf(f64::NAN), fprf::QNAN);
        assert_eq!(classify_fprf(f64::from_bits(1)), fprf::POS_DENORMAL);
    }

    #[test]
    fn fx_is_sticky_on_new_exception() {
        let mut c = ctx();
        set_exception(&mut c, OX);
        assert_ne!(c.fpscr & FX, 0);
        // Clear FX/OX manually.
        c.fpscr &= !(FX | OX);
        // Re-set OX; FX should re-latch.
        set_exception(&mut c, OX);
        assert_ne!(c.fpscr & FX, 0);
    }

    #[test]
    fn vx_summary_set_on_any_vx_bit() {
        let mut c = ctx();
        set_exception(&mut c, VXSNAN);
        assert_ne!(c.fpscr & VX, 0);
        assert_ne!(c.fpscr & VXSNAN, 0);
    }

    #[test]
    fn round_to_single_nearest_is_identity_on_representable() {
        let c = ctx();
        assert_eq!(round_to_single(&c, 1.0_f64), 1.0_f64);
    }

    #[test]
    fn round_to_i32_clamps_out_of_range() {
        let c = ctx();
        assert_eq!(round_to_i32(&c, 1e20_f64), i32::MAX);
        assert_eq!(round_to_i32(&c, -1e20_f64), i32::MIN);
    }

    #[test]
    fn round_to_i64_nearest_even_on_tie() {
        let c = ctx();
        assert_eq!(round_to_i64(&c, 0.5_f64), 0);
        assert_eq!(round_to_i64(&c, 1.5_f64), 2);
        assert_eq!(round_to_i64(&c, 2.5_f64), 2);
        assert_eq!(round_to_i64(&c, 3.5_f64), 4);
        assert_eq!(round_to_i64(&c, -0.5_f64), 0);
        assert_eq!(round_to_i64(&c, -1.5_f64), -2);
        assert_eq!(round_to_i64(&c, -2.5_f64), -2);
    }

    #[test]
    fn round_to_i64_non_tie_cases() {
        // PPCBUG-221 regression: non-tie fractions must round to nearest.
        let c = ctx();
        assert_eq!(round_to_i64(&c, 0.4_f64), 0);
        assert_eq!(round_to_i64(&c, 0.6_f64), 1);
        assert_eq!(round_to_i64(&c, -0.4_f64), 0);
        assert_eq!(round_to_i64(&c, -0.6_f64), -1);
    }

    #[test]
    fn round_to_i32_nearest_even_on_tie() {
        // PPCBUG-227: round_to_i32 inherits round_to_i64's tie semantics.
        let c = ctx();
        assert_eq!(round_to_i32(&c, 0.5_f64), 0);
        assert_eq!(round_to_i32(&c, 1.5_f64), 2);
        assert_eq!(round_to_i32(&c, 2.5_f64), 2);
        assert_eq!(round_to_i32(&c, -1.5_f64), -2);
    }

    #[test]
    fn check_invalid_add_detects_inf_minus_inf() {
        let mut c = ctx();
        assert!(check_invalid_add(&mut c, f64::INFINITY, f64::INFINITY, true));
        assert_ne!(c.fpscr & VXISI, 0);
    }

    #[test]
    fn check_invalid_div_detects_zero_over_zero() {
        let mut c = ctx();
        assert!(check_invalid_div(&mut c, 0.0, 0.0));
        assert_ne!(c.fpscr & VXZDZ, 0);
    }

    #[test]
    fn snan_detection() {
        // SNaN in binary64: sign=0, exp=all-ones, mantissa nonzero with bit 51 clear.
        let snan = f64::from_bits(0x7FF0_0000_0000_0001);
        assert!(is_snan(snan));
        assert!(!is_snan(f64::NAN));
    }
}
