//! Trigonometric functions: sin, cos, tan, asin, acos, atan, sinh, cosh, tanh.
//!
//! All trig functions are angle-mode aware. When `AngleMode::Degrees` is active
//! (or when `force_degrees` is true), inputs to forward trig functions are
//! converted from degrees to radians, and outputs of inverse trig functions
//! are converted from radians to degrees. Hyperbolic functions ignore angle mode.

use super::{AngleMode, FunctionError, FunctionRegistry};

const DEG_TO_RAD: f64 = std::f64::consts::PI / 180.0;
const RAD_TO_DEG: f64 = 180.0 / std::f64::consts::PI;

fn to_radians(value: f64, mode: AngleMode, force_degrees: bool) -> f64 {
    if force_degrees || mode == AngleMode::Degrees {
        value * DEG_TO_RAD
    } else {
        value
    }
}

fn from_radians(value: f64, mode: AngleMode) -> f64 {
    if mode == AngleMode::Degrees {
        value * RAD_TO_DEG
    } else {
        value
    }
}

// --- forward trig ---

fn sin_fn(arg: f64, mode: AngleMode, force_deg: bool) -> Result<f64, FunctionError> {
    Ok(to_radians(arg, mode, force_deg).sin())
}

fn cos_fn(arg: f64, mode: AngleMode, force_deg: bool) -> Result<f64, FunctionError> {
    Ok(to_radians(arg, mode, force_deg).cos())
}

fn tan_fn(arg: f64, mode: AngleMode, force_deg: bool) -> Result<f64, FunctionError> {
    Ok(to_radians(arg, mode, force_deg).tan())
}

// --- inverse trig ---

fn asin_fn(arg: f64, mode: AngleMode, _force_deg: bool) -> Result<f64, FunctionError> {
    if arg < -1.0 || arg > 1.0 {
        return Err(FunctionError::new(
            "Argument out of domain for asin (must be between -1 and 1)",
        ));
    }
    Ok(from_radians(arg.asin(), mode))
}

fn acos_fn(arg: f64, mode: AngleMode, _force_deg: bool) -> Result<f64, FunctionError> {
    if arg < -1.0 || arg > 1.0 {
        return Err(FunctionError::new(
            "Argument out of domain for acos (must be between -1 and 1)",
        ));
    }
    Ok(from_radians(arg.acos(), mode))
}

fn atan_fn(arg: f64, mode: AngleMode, _force_deg: bool) -> Result<f64, FunctionError> {
    Ok(from_radians(arg.atan(), mode))
}

// --- hyperbolic (angle-mode independent) ---

fn sinh_fn(arg: f64, _mode: AngleMode, _force_deg: bool) -> Result<f64, FunctionError> {
    Ok(arg.sinh())
}

fn cosh_fn(arg: f64, _mode: AngleMode, _force_deg: bool) -> Result<f64, FunctionError> {
    Ok(arg.cosh())
}

fn tanh_fn(arg: f64, _mode: AngleMode, _force_deg: bool) -> Result<f64, FunctionError> {
    Ok(arg.tanh())
}

/// Register all trig functions into the given registry.
pub fn register(reg: &mut FunctionRegistry) {
    reg.register_trig("sin", sin_fn);
    reg.register_trig("cos", cos_fn);
    reg.register_trig("tan", tan_fn);
    reg.register_trig("asin", asin_fn);
    reg.register_trig("acos", acos_fn);
    reg.register_trig("atan", atan_fn);
    reg.register_trig("sinh", sinh_fn);
    reg.register_trig("cosh", cosh_fn);
    reg.register_trig("tanh", tanh_fn);
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI, SQRT_2};

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

    // --- radians mode (default) ---

    #[test]
    fn sin_zero_radians() {
        let r = reg();
        let v = r.call_trig("sin", 0.0, AngleMode::Radians, false).unwrap();
        assert!((v - 0.0).abs() < 1e-10);
    }

    #[test]
    fn sin_pi_radians() {
        let r = reg();
        let v = r.call_trig("sin", PI, AngleMode::Radians, false).unwrap();
        assert!(v.abs() < 1e-10);
    }

    #[test]
    fn cos_zero_is_one() {
        let r = reg();
        let v = r.call_trig("cos", 0.0, AngleMode::Radians, false).unwrap();
        assert!((v - 1.0).abs() < 1e-10);
    }

    #[test]
    fn tan_zero_is_zero() {
        let r = reg();
        let v = r.call_trig("tan", 0.0, AngleMode::Radians, false).unwrap();
        assert!(v.abs() < 1e-10);
    }

    #[test]
    fn asin_one_is_pi_over_2() {
        let r = reg();
        let v = r.call_trig("asin", 1.0, AngleMode::Radians, false).unwrap();
        assert!((v - FRAC_PI_2).abs() < 1e-10);
    }

    #[test]
    fn acos_one_is_zero() {
        let r = reg();
        let v = r.call_trig("acos", 1.0, AngleMode::Radians, false).unwrap();
        assert!(v.abs() < 1e-10);
    }

    #[test]
    fn atan_one_is_pi_over_4() {
        let r = reg();
        let v = r.call_trig("atan", 1.0, AngleMode::Radians, false).unwrap();
        assert!((v - FRAC_PI_4).abs() < 1e-10);
    }

    // --- degrees mode ---

    #[test]
    fn sin_90_degrees_is_one() {
        let r = reg();
        let v = r.call_trig("sin", 90.0, AngleMode::Degrees, false).unwrap();
        assert!((v - 1.0).abs() < 1e-10);
    }

    #[test]
    fn cos_zero_degrees_is_one() {
        let r = reg();
        let v = r.call_trig("cos", 0.0, AngleMode::Degrees, false).unwrap();
        assert!((v - 1.0).abs() < 1e-10);
    }

    #[test]
    fn acos_half_degrees_is_60() {
        let r = reg();
        let v = r
            .call_trig("acos", 0.5, AngleMode::Degrees, false)
            .unwrap();
        assert!((v - 60.0).abs() < 1e-10);
    }

    #[test]
    fn atan_one_degrees_is_45() {
        let r = reg();
        let v = r
            .call_trig("atan", 1.0, AngleMode::Degrees, false)
            .unwrap();
        assert!((v - 45.0).abs() < 1e-10);
    }

    // --- force-degrees override (radians mode, but degree symbol present) ---

    #[test]
    fn sin_45_force_degrees_in_rad_mode() {
        let r = reg();
        let v = r.call_trig("sin", 45.0, AngleMode::Radians, true).unwrap();
        assert!((v - SQRT_2 / 2.0).abs() < 1e-6);
    }

    #[test]
    fn tan_45_force_degrees() {
        let r = reg();
        let v = r.call_trig("tan", 45.0, AngleMode::Radians, true).unwrap();
        assert!((v - 1.0).abs() < 1e-10);
    }

    // --- hyperbolic ---

    #[test]
    fn sinh_one() {
        let r = reg();
        let v = r.call_trig("sinh", 1.0, AngleMode::Radians, false).unwrap();
        assert!((v - 1.1752011936438014).abs() < 1e-6);
    }

    #[test]
    fn cosh_zero_is_one() {
        let r = reg();
        let v = r.call_trig("cosh", 0.0, AngleMode::Radians, false).unwrap();
        assert!((v - 1.0).abs() < 1e-10);
    }

    #[test]
    fn tanh_zero_is_zero() {
        let r = reg();
        let v = r.call_trig("tanh", 0.0, AngleMode::Radians, false).unwrap();
        assert!(v.abs() < 1e-10);
    }

    // --- domain errors ---

    #[test]
    fn asin_out_of_domain() {
        let r = reg();
        let err = r
            .call_trig("asin", 2.0, AngleMode::Radians, false)
            .unwrap_err();
        assert!(err.message.contains("out of domain"));
    }

    #[test]
    fn asin_negative_out_of_domain() {
        let r = reg();
        let err = r
            .call_trig("asin", -2.0, AngleMode::Radians, false)
            .unwrap_err();
        assert!(err.message.contains("out of domain"));
    }

    #[test]
    fn acos_out_of_domain() {
        let r = reg();
        let err = r
            .call_trig("acos", 2.0, AngleMode::Radians, false)
            .unwrap_err();
        assert!(err.message.contains("out of domain"));
    }
}
