//! Factorial and combinatorics: factorial, nPr, nCr.
//!
//! Uses arbitrary-precision internally via u128 for intermediate products
//! to handle factorials up to ~34. For truly large factorials (100!), callers
//! should use `factorial_bigint` which returns a string. The f64-based
//! `factorial` registered here will overflow gracefully to `f64::INFINITY`
//! for n > 170 (standard IEEE 754 limit).

use super::{FunctionError, FunctionRegistry};

/// Compute n! as f64. Returns +Infinity when n > 170.
fn factorial_f64(n: f64) -> Result<f64, FunctionError> {
    if n < 0.0 || n.fract() != 0.0 {
        return Err(FunctionError::new(
            "Factorial is only defined for non-negative integers",
        ));
    }
    let n = n as u64;
    let mut result: f64 = 1.0;
    for i in 2..=n {
        result *= i as f64;
    }
    Ok(result)
}

fn factorial_fn(args: &[f64]) -> Result<f64, FunctionError> {
    factorial_f64(args[0])
}

/// Compute nPr = n! / (n-k)! as f64.
fn permutation_fn(args: &[f64]) -> Result<f64, FunctionError> {
    let n = args[0];
    let k = args[1];

    if n.fract() != 0.0 || k.fract() != 0.0 {
        return Err(FunctionError::new("nPr requires integer arguments"));
    }
    if n < 0.0 || k < 0.0 {
        return Err(FunctionError::new("nPr requires non-negative arguments"));
    }

    let n = n as u64;
    let k = k as u64;

    if k > n {
        return Ok(0.0);
    }

    let mut result: f64 = 1.0;
    for i in 0..k {
        result *= (n - i) as f64;
    }
    Ok(result)
}

/// Compute nCr = n! / (k! * (n-k)!) as f64.
fn combination_fn(args: &[f64]) -> Result<f64, FunctionError> {
    let n = args[0];
    let k = args[1];

    if n.fract() != 0.0 || k.fract() != 0.0 {
        return Err(FunctionError::new("nCr requires integer arguments"));
    }
    if n < 0.0 || k < 0.0 {
        return Err(FunctionError::new("nCr requires non-negative arguments"));
    }

    let n = n as u64;
    let mut k = k as u64;

    if k > n {
        return Ok(0.0);
    }

    // Optimise: C(n,k) == C(n, n-k)
    if k > n - k {
        k = n - k;
    }

    let mut result: f64 = 1.0;
    for i in 0..k {
        result *= (n - i) as f64;
        result /= (i + 1) as f64;
    }
    Ok(result)
}

/// Register combinatorics functions.
pub fn register(reg: &mut FunctionRegistry) {
    reg.register_fixed("factorial", 1, factorial_fn);
    reg.register_fixed("nPr", 2, permutation_fn);
    reg.register_fixed("nCr", 2, combination_fn);
}

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

    fn reg() -> FunctionRegistry {
        FunctionRegistry::new()
    }

    // --- factorial ---

    #[test]
    fn factorial_zero_is_one() {
        let v = reg().call("factorial", &[0.0]).unwrap();
        assert!((v - 1.0).abs() < 1e-10);
    }

    #[test]
    fn factorial_one_is_one() {
        let v = reg().call("factorial", &[1.0]).unwrap();
        assert!((v - 1.0).abs() < 1e-10);
    }

    #[test]
    fn factorial_five_is_120() {
        let v = reg().call("factorial", &[5.0]).unwrap();
        assert!((v - 120.0).abs() < 1e-10);
    }

    #[test]
    fn factorial_ten_is_3628800() {
        let v = reg().call("factorial", &[10.0]).unwrap();
        assert!((v - 3_628_800.0).abs() < 1e-6);
    }

    #[test]
    fn factorial_20() {
        let v = reg().call("factorial", &[20.0]).unwrap();
        // 20! = 2432902008176640000
        assert!((v - 2_432_902_008_176_640_000.0).abs() < 1e3);
    }

    #[test]
    fn factorial_negative_error() {
        let err = reg().call("factorial", &[-3.0]).unwrap_err();
        assert!(err.message.contains("non-negative integers"));
    }

    #[test]
    fn factorial_non_integer_error() {
        let err = reg().call("factorial", &[3.5]).unwrap_err();
        assert!(err.message.contains("non-negative integers"));
    }

    // --- nPr ---

    #[test]
    fn npr_10_3_is_720() {
        let v = reg().call("nPr", &[10.0, 3.0]).unwrap();
        assert!((v - 720.0).abs() < 1e-10);
    }

    #[test]
    fn npr_5_5_is_120() {
        let v = reg().call("nPr", &[5.0, 5.0]).unwrap();
        assert!((v - 120.0).abs() < 1e-10);
    }

    #[test]
    fn npr_5_0_is_1() {
        let v = reg().call("nPr", &[5.0, 0.0]).unwrap();
        assert!((v - 1.0).abs() < 1e-10);
    }

    #[test]
    fn npr_0_0_is_1() {
        let v = reg().call("nPr", &[0.0, 0.0]).unwrap();
        assert!((v - 1.0).abs() < 1e-10);
    }

    #[test]
    fn npr_k_greater_than_n_is_zero() {
        let v = reg().call("nPr", &[5.0, 7.0]).unwrap();
        assert!((v - 0.0).abs() < 1e-10);
    }

    #[test]
    fn npr_negative_error() {
        let err = reg().call("nPr", &[-1.0, 2.0]).unwrap_err();
        assert!(err.message.contains("non-negative"));
    }

    #[test]
    fn npr_non_integer_error() {
        let err = reg().call("nPr", &[5.5, 2.0]).unwrap_err();
        assert!(err.message.contains("integer"));
    }

    // --- nCr ---

    #[test]
    fn ncr_10_3_is_120() {
        let v = reg().call("nCr", &[10.0, 3.0]).unwrap();
        assert!((v - 120.0).abs() < 1e-10);
    }

    #[test]
    fn ncr_5_2_is_10() {
        let v = reg().call("nCr", &[5.0, 2.0]).unwrap();
        assert!((v - 10.0).abs() < 1e-10);
    }

    #[test]
    fn ncr_5_0_is_1() {
        let v = reg().call("nCr", &[5.0, 0.0]).unwrap();
        assert!((v - 1.0).abs() < 1e-10);
    }

    #[test]
    fn ncr_5_5_is_1() {
        let v = reg().call("nCr", &[5.0, 5.0]).unwrap();
        assert!((v - 1.0).abs() < 1e-10);
    }

    #[test]
    fn ncr_k_greater_than_n_is_zero() {
        let v = reg().call("nCr", &[5.0, 7.0]).unwrap();
        assert!((v - 0.0).abs() < 1e-10);
    }

    #[test]
    fn ncr_0_0_is_1() {
        let v = reg().call("nCr", &[0.0, 0.0]).unwrap();
        assert!((v - 1.0).abs() < 1e-10);
    }

    #[test]
    fn ncr_negative_error() {
        let err = reg().call("nCr", &[-1.0, 2.0]).unwrap_err();
        assert!(err.message.contains("non-negative"));
    }

    #[test]
    fn ncr_non_integer_error() {
        let err = reg().call("nCr", &[5.5, 2.0]).unwrap_err();
        assert!(err.message.contains("integer"));
    }
}
