//! Function registry and dispatch for CalcPad math functions.
//!
//! Provides a [`FunctionRegistry`] that maps function names to typed
//! implementations across all function categories (trig, logarithmic,
//! combinatorics, financial, rounding, list ops, timecodes).

pub mod combinatorics;
pub mod financial;
pub mod list_ops;
pub mod logarithmic;
pub mod rounding;
pub mod timecodes;
pub mod trig;

use std::collections::HashMap;

/// Error type returned by function evaluation.
#[derive(Debug, Clone, PartialEq)]
pub struct FunctionError {
    pub message: String,
}

impl FunctionError {
    pub fn new(msg: impl Into<String>) -> Self {
        Self {
            message: msg.into(),
        }
    }
}

impl std::fmt::Display for FunctionError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.message)
    }
}

impl std::error::Error for FunctionError {}

/// Angle mode for trigonometric functions.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AngleMode {
    Radians,
    Degrees,
}

impl Default for AngleMode {
    fn default() -> Self {
        AngleMode::Radians
    }
}

/// The signature of a function: how many args it accepts and how to call it.
#[derive(Clone)]
enum FnImpl {
    /// Fixed-arity function (e.g. sin takes 1 arg, pow takes 2).
    Fixed {
        arity: usize,
        func: fn(&[f64]) -> Result<f64, FunctionError>,
    },
    /// Variadic function that accepts 1..N args (e.g. min, max, gcd, lcm).
    Variadic {
        min_args: usize,
        func: fn(&[f64]) -> Result<f64, FunctionError>,
    },
    /// Angle-aware trig function (1 arg + angle mode + force-degrees flag).
    Trig {
        func: fn(f64, AngleMode, bool) -> Result<f64, FunctionError>,
    },
    /// Variable-arity function with a known range (e.g. round takes 1 or 2).
    RangeArity {
        min_args: usize,
        max_args: usize,
        func: fn(&[f64]) -> Result<f64, FunctionError>,
    },
    /// Timecode function that operates on string-like frame values.
    Timecode {
        func: fn(&[f64]) -> Result<f64, FunctionError>,
    },
}

/// Central registry mapping function names to their implementations.
pub struct FunctionRegistry {
    functions: HashMap<String, FnImpl>,
}

impl FunctionRegistry {
    /// Build a new registry pre-loaded with all built-in functions.
    pub fn new() -> Self {
        let mut reg = Self {
            functions: HashMap::new(),
        };
        trig::register(&mut reg);
        logarithmic::register(&mut reg);
        combinatorics::register(&mut reg);
        financial::register(&mut reg);
        rounding::register(&mut reg);
        list_ops::register(&mut reg);
        timecodes::register(&mut reg);
        reg
    }

    // ---- registration helpers (called by sub-modules) ----

    pub(crate) fn register_trig(
        &mut self,
        name: &str,
        func: fn(f64, AngleMode, bool) -> Result<f64, FunctionError>,
    ) {
        self.functions
            .insert(name.to_string(), FnImpl::Trig { func });
    }

    pub(crate) fn register_fixed(
        &mut self,
        name: &str,
        arity: usize,
        func: fn(&[f64]) -> Result<f64, FunctionError>,
    ) {
        self.functions
            .insert(name.to_string(), FnImpl::Fixed { arity, func });
    }

    pub(crate) fn register_variadic(
        &mut self,
        name: &str,
        min_args: usize,
        func: fn(&[f64]) -> Result<f64, FunctionError>,
    ) {
        self.functions
            .insert(name.to_string(), FnImpl::Variadic { min_args, func });
    }

    pub(crate) fn register_range_arity(
        &mut self,
        name: &str,
        min_args: usize,
        max_args: usize,
        func: fn(&[f64]) -> Result<f64, FunctionError>,
    ) {
        self.functions.insert(
            name.to_string(),
            FnImpl::RangeArity {
                min_args,
                max_args,
                func,
            },
        );
    }

    #[allow(dead_code)]
    pub(crate) fn register_timecode(
        &mut self,
        name: &str,
        func: fn(&[f64]) -> Result<f64, FunctionError>,
    ) {
        self.functions
            .insert(name.to_string(), FnImpl::Timecode { func });
    }

    // ---- dispatch ----

    /// Returns true if `name` is a registered function.
    pub fn has_function(&self, name: &str) -> bool {
        self.functions.contains_key(name)
    }

    /// Returns true if `name` is a trig function (needs angle mode).
    pub fn is_trig(&self, name: &str) -> bool {
        matches!(self.functions.get(name), Some(FnImpl::Trig { .. }))
    }

    /// Call a trig function with the given argument, angle mode, and
    /// force-degrees flag.
    pub fn call_trig(
        &self,
        name: &str,
        arg: f64,
        mode: AngleMode,
        force_degrees: bool,
    ) -> Result<f64, FunctionError> {
        match self.functions.get(name) {
            Some(FnImpl::Trig { func }) => func(arg, mode, force_degrees),
            Some(_) => Err(FunctionError::new(format!(
                "{} is not a trigonometric function",
                name
            ))),
            None => Err(FunctionError::new(format!("Unknown function: {}", name))),
        }
    }

    /// Call any non-trig function with a slice of evaluated arguments.
    pub fn call(&self, name: &str, args: &[f64]) -> Result<f64, FunctionError> {
        match self.functions.get(name) {
            Some(FnImpl::Fixed { arity, func }) => {
                if args.len() != *arity {
                    return Err(FunctionError::new(format!(
                        "{} expects {} argument(s), got {}",
                        name,
                        arity,
                        args.len()
                    )));
                }
                func(args)
            }
            Some(FnImpl::Variadic { min_args, func }) => {
                if args.len() < *min_args {
                    return Err(FunctionError::new(format!(
                        "{} requires at least {} argument(s), got {}",
                        name,
                        min_args,
                        args.len()
                    )));
                }
                func(args)
            }
            Some(FnImpl::RangeArity {
                min_args,
                max_args,
                func,
            }) => {
                if args.len() < *min_args || args.len() > *max_args {
                    return Err(FunctionError::new(format!(
                        "{} expects {}-{} argument(s), got {}",
                        name,
                        min_args,
                        max_args,
                        args.len()
                    )));
                }
                func(args)
            }
            Some(FnImpl::Trig { func }) => {
                // Convenience: if called via `call()`, default to radians, no force-degrees.
                if args.len() != 1 {
                    return Err(FunctionError::new(format!(
                        "{} expects 1 argument, got {}",
                        name,
                        args.len()
                    )));
                }
                func(args[0], AngleMode::Radians, false)
            }
            Some(FnImpl::Timecode { func }) => func(args),
            None => Err(FunctionError::new(format!("Unknown function: {}", name))),
        }
    }

    /// List all registered function names (sorted alphabetically).
    pub fn function_names(&self) -> Vec<&str> {
        let mut names: Vec<&str> = self.functions.keys().map(|s| s.as_str()).collect();
        names.sort();
        names
    }
}

impl Default for FunctionRegistry {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn registry_contains_all_categories() {
        let reg = FunctionRegistry::new();
        // Trig
        assert!(reg.has_function("sin"));
        assert!(reg.has_function("cos"));
        assert!(reg.has_function("tanh"));
        // Logarithmic
        assert!(reg.has_function("ln"));
        assert!(reg.has_function("log"));
        assert!(reg.has_function("sqrt"));
        // Combinatorics
        assert!(reg.has_function("factorial"));
        assert!(reg.has_function("nPr"));
        assert!(reg.has_function("nCr"));
        // Financial
        assert!(reg.has_function("compound_interest"));
        assert!(reg.has_function("mortgage_payment"));
        // Rounding
        assert!(reg.has_function("round"));
        assert!(reg.has_function("floor"));
        assert!(reg.has_function("ceil"));
        // List
        assert!(reg.has_function("min"));
        assert!(reg.has_function("max"));
        assert!(reg.has_function("gcd"));
        assert!(reg.has_function("lcm"));
        // Timecodes
        assert!(reg.has_function("tc_to_frames"));
        assert!(reg.has_function("frames_to_tc"));
    }

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

    #[test]
    fn call_unknown_function_returns_error() {
        let reg = FunctionRegistry::new();
        let err = reg.call("nonexistent_fn", &[1.0]).unwrap_err();
        assert!(err.message.contains("Unknown function"));
    }

    #[test]
    fn arity_mismatch_returns_error() {
        let reg = FunctionRegistry::new();
        let err = reg.call("sqrt", &[1.0, 2.0]).unwrap_err();
        assert!(err.message.contains("expects 1 argument"));
    }
}
