//! Video timecode arithmetic.
//!
//! Timecodes represent positions in video as `HH:MM:SS:FF` where FF is a
//! frame count within the current second. The number of frames per second
//! (fps) determines the range of FF (0..fps-1).
//!
//! ## Functions
//!
//! - `tc_to_frames(hours, minutes, seconds, frames, fps)` — convert a
//!   timecode to a total frame count.
//! - `frames_to_tc(total_frames, fps)` — convert total frames back to a
//!   packed timecode value `HH * 1_000_000 + MM * 10_000 + SS * 100 + FF`
//!   for easy extraction of components.
//! - `tc_add_frames(hours, minutes, seconds, frames, fps, add_frames)` —
//!   add (or subtract) a number of frames to a timecode and return the new
//!   total frame count.
//!
//! Common fps values: 24, 25, 29.97 (NTSC drop-frame), 30, 48, 60.
//!
//! For now we work in non-drop-frame (NDF) mode. Drop-frame support can be
//! added later.

use super::{FunctionError, FunctionRegistry};

/// Convert a timecode (H, M, S, F, fps) to total frame count.
fn tc_to_frames_fn(args: &[f64]) -> Result<f64, FunctionError> {
    if args.len() != 5 {
        return Err(FunctionError::new(
            "tc_to_frames expects 5 arguments: hours, minutes, seconds, frames, fps",
        ));
    }
    let hours = args[0];
    let minutes = args[1];
    let seconds = args[2];
    let frames = args[3];
    let fps = args[4];

    validate_timecode_components(hours, minutes, seconds, frames, fps)?;

    let fps_i = fps as u64;
    let total = (hours as u64) * 3600 * fps_i
        + (minutes as u64) * 60 * fps_i
        + (seconds as u64) * fps_i
        + (frames as u64);
    Ok(total as f64)
}

/// Convert total frames to a packed timecode: HH*1_000_000 + MM*10_000 + SS*100 + FF.
/// Returns the packed value. Also returns components via the packed encoding.
fn frames_to_tc_fn(args: &[f64]) -> Result<f64, FunctionError> {
    if args.len() != 2 {
        return Err(FunctionError::new(
            "frames_to_tc expects 2 arguments: total_frames, fps",
        ));
    }
    let total = args[0];
    let fps = args[1];

    if total < 0.0 || total.fract() != 0.0 {
        return Err(FunctionError::new(
            "total_frames must be a non-negative integer",
        ));
    }
    if fps <= 0.0 || fps.fract() != 0.0 {
        return Err(FunctionError::new(
            "fps must be a positive integer",
        ));
    }

    let total = total as u64;
    let fps_i = fps as u64;

    let ff = total % fps_i;
    let rem = total / fps_i;
    let ss = rem % 60;
    let rem = rem / 60;
    let mm = rem % 60;
    let hh = rem / 60;

    // Pack into a single number: HH_MM_SS_FF
    let packed = hh * 1_000_000 + mm * 10_000 + ss * 100 + ff;
    Ok(packed as f64)
}

/// Add frames to a timecode and return new total frame count.
fn tc_add_frames_fn(args: &[f64]) -> Result<f64, FunctionError> {
    if args.len() != 6 {
        return Err(FunctionError::new(
            "tc_add_frames expects 6 arguments: hours, minutes, seconds, frames, fps, add_frames",
        ));
    }
    let hours = args[0];
    let minutes = args[1];
    let seconds = args[2];
    let frames = args[3];
    let fps = args[4];
    let add_frames = args[5];

    validate_timecode_components(hours, minutes, seconds, frames, fps)?;

    let fps_i = fps as u64;
    let total = (hours as u64) * 3600 * fps_i
        + (minutes as u64) * 60 * fps_i
        + (seconds as u64) * fps_i
        + (frames as u64);

    let new_total = total as i64 + add_frames as i64;
    if new_total < 0 {
        return Err(FunctionError::new(
            "Resulting timecode would be negative",
        ));
    }

    Ok(new_total as f64)
}

fn validate_timecode_components(
    hours: f64,
    minutes: f64,
    seconds: f64,
    frames: f64,
    fps: f64,
) -> Result<(), FunctionError> {
    if fps <= 0.0 || fps.fract() != 0.0 {
        return Err(FunctionError::new("fps must be a positive integer"));
    }
    if hours < 0.0 || hours.fract() != 0.0 {
        return Err(FunctionError::new(
            "hours must be a non-negative integer",
        ));
    }
    if minutes < 0.0 || minutes >= 60.0 || minutes.fract() != 0.0 {
        return Err(FunctionError::new(
            "minutes must be an integer in 0..59",
        ));
    }
    if seconds < 0.0 || seconds >= 60.0 || seconds.fract() != 0.0 {
        return Err(FunctionError::new(
            "seconds must be an integer in 0..59",
        ));
    }
    if frames < 0.0 || frames >= fps || frames.fract() != 0.0 {
        return Err(FunctionError::new(format!(
            "frames must be an integer in 0..{} (fps={})",
            fps as u64 - 1,
            fps as u64,
        )));
    }
    Ok(())
}

/// Register timecode functions.
pub fn register(reg: &mut FunctionRegistry) {
    reg.register_fixed("tc_to_frames", 5, tc_to_frames_fn);
    reg.register_fixed("frames_to_tc", 2, frames_to_tc_fn);
    reg.register_fixed("tc_add_frames", 6, tc_add_frames_fn);
}

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

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

    // --- tc_to_frames ---

    #[test]
    fn tc_to_frames_zero() {
        // 00:00:00:00 at 24fps => 0 frames
        let v = reg()
            .call("tc_to_frames", &[0.0, 0.0, 0.0, 0.0, 24.0])
            .unwrap();
        assert!((v - 0.0).abs() < 1e-10);
    }

    #[test]
    fn tc_to_frames_one_second_24fps() {
        // 00:00:01:00 at 24fps => 24 frames
        let v = reg()
            .call("tc_to_frames", &[0.0, 0.0, 1.0, 0.0, 24.0])
            .unwrap();
        assert!((v - 24.0).abs() < 1e-10);
    }

    #[test]
    fn tc_to_frames_one_minute_24fps() {
        // 00:01:00:00 at 24fps => 1440 frames
        let v = reg()
            .call("tc_to_frames", &[0.0, 1.0, 0.0, 0.0, 24.0])
            .unwrap();
        assert!((v - 1440.0).abs() < 1e-10);
    }

    #[test]
    fn tc_to_frames_one_hour_24fps() {
        // 01:00:00:00 at 24fps => 86400 frames
        let v = reg()
            .call("tc_to_frames", &[1.0, 0.0, 0.0, 0.0, 24.0])
            .unwrap();
        assert!((v - 86400.0).abs() < 1e-10);
    }

    #[test]
    fn tc_to_frames_mixed() {
        // 01:02:03:04 at 24fps => 1*3600*24 + 2*60*24 + 3*24 + 4 = 86400 + 2880 + 72 + 4 = 89356
        let v = reg()
            .call("tc_to_frames", &[1.0, 2.0, 3.0, 4.0, 24.0])
            .unwrap();
        assert!((v - 89356.0).abs() < 1e-10);
    }

    #[test]
    fn tc_to_frames_25fps() {
        // 00:00:01:00 at 25fps => 25 frames
        let v = reg()
            .call("tc_to_frames", &[0.0, 0.0, 1.0, 0.0, 25.0])
            .unwrap();
        assert!((v - 25.0).abs() < 1e-10);
    }

    #[test]
    fn tc_to_frames_30fps() {
        // 00:00:01:00 at 30fps => 30 frames
        let v = reg()
            .call("tc_to_frames", &[0.0, 0.0, 1.0, 0.0, 30.0])
            .unwrap();
        assert!((v - 30.0).abs() < 1e-10);
    }

    #[test]
    fn tc_to_frames_60fps() {
        // 00:01:00:00 at 60fps => 3600 frames
        let v = reg()
            .call("tc_to_frames", &[0.0, 1.0, 0.0, 0.0, 60.0])
            .unwrap();
        assert!((v - 3600.0).abs() < 1e-10);
    }

    #[test]
    fn tc_to_frames_invalid_minutes() {
        let err = reg()
            .call("tc_to_frames", &[0.0, 60.0, 0.0, 0.0, 24.0])
            .unwrap_err();
        assert!(err.message.contains("minutes"));
    }

    #[test]
    fn tc_to_frames_invalid_frames() {
        // Frames >= fps is invalid
        let err = reg()
            .call("tc_to_frames", &[0.0, 0.0, 0.0, 24.0, 24.0])
            .unwrap_err();
        assert!(err.message.contains("frames"));
    }

    #[test]
    fn tc_to_frames_invalid_fps() {
        let err = reg()
            .call("tc_to_frames", &[0.0, 0.0, 0.0, 0.0, 0.0])
            .unwrap_err();
        assert!(err.message.contains("fps"));
    }

    // --- frames_to_tc ---

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

    #[test]
    fn frames_to_tc_one_second() {
        // 24 frames at 24fps => 00:00:01:00 => packed 100
        let v = reg().call("frames_to_tc", &[24.0, 24.0]).unwrap();
        assert!((v - 100.0).abs() < 1e-10);
    }

    #[test]
    fn frames_to_tc_one_minute() {
        // 1440 frames at 24fps => 00:01:00:00 => packed 10000
        let v = reg().call("frames_to_tc", &[1440.0, 24.0]).unwrap();
        assert!((v - 10000.0).abs() < 1e-10);
    }

    #[test]
    fn frames_to_tc_one_hour() {
        // 86400 frames at 24fps => 01:00:00:00 => packed 1000000
        let v = reg().call("frames_to_tc", &[86400.0, 24.0]).unwrap();
        assert!((v - 1_000_000.0).abs() < 1e-10);
    }

    #[test]
    fn frames_to_tc_mixed() {
        // 89356 frames at 24fps => 01:02:03:04 => packed 1020304
        let v = reg().call("frames_to_tc", &[89356.0, 24.0]).unwrap();
        assert!((v - 1_020_304.0).abs() < 1e-10);
    }

    #[test]
    fn frames_to_tc_roundtrip() {
        let r = reg();
        // Convert to frames then back
        let frames = r
            .call("tc_to_frames", &[2.0, 30.0, 15.0, 12.0, 30.0])
            .unwrap();
        let packed = r.call("frames_to_tc", &[frames, 30.0]).unwrap();
        // 02:30:15:12 => packed 2301512
        assert!((packed - 2_301_512.0).abs() < 1e-10);
    }

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

    // --- tc_add_frames ---

    #[test]
    fn tc_add_frames_simple() {
        let r = reg();
        // 00:00:00:00 at 24fps + 48 frames => 48
        let v = r
            .call("tc_add_frames", &[0.0, 0.0, 0.0, 0.0, 24.0, 48.0])
            .unwrap();
        assert!((v - 48.0).abs() < 1e-10);
    }

    #[test]
    fn tc_add_frames_subtract() {
        let r = reg();
        // 00:00:02:00 at 24fps = 48 frames, subtract 24 => 24
        let v = r
            .call("tc_add_frames", &[0.0, 0.0, 2.0, 0.0, 24.0, -24.0])
            .unwrap();
        assert!((v - 24.0).abs() < 1e-10);
    }

    #[test]
    fn tc_add_frames_negative_result_error() {
        let r = reg();
        let err = r
            .call("tc_add_frames", &[0.0, 0.0, 0.0, 0.0, 24.0, -1.0])
            .unwrap_err();
        assert!(err.message.contains("negative"));
    }

    #[test]
    fn tc_add_frames_cross_minute_boundary() {
        let r = reg();
        // 00:00:59:23 at 24fps + 1 frame
        let base = r
            .call("tc_to_frames", &[0.0, 0.0, 59.0, 23.0, 24.0])
            .unwrap();
        let v = r
            .call("tc_add_frames", &[0.0, 0.0, 59.0, 23.0, 24.0, 1.0])
            .unwrap();
        assert!((v - (base + 1.0)).abs() < 1e-10);
        // Verify the result converts to 00:01:00:00
        let packed = r.call("frames_to_tc", &[v, 24.0]).unwrap();
        assert!((packed - 10000.0).abs() < 1e-10);
    }
}
