//! Frame-based time representation for video processing.
//!
//! This module provides a `FrameTime` struct that stores time as frame count
//! with a given FPS (frames per second). This avoids floating-point precision
//! issues when converting between seconds and frames.
//!
//! # Examples
//!
//! ```
//! use momentry_core::time::FrameTime;
//!
//! // Create a FrameTime from frames
//! let time = FrameTime::from_frames(1234, 30.0);
//! assert_eq!(time.seconds(), 41.13333333333333);
//! assert_eq!(time.format_sec_frame(), "41.04");
//!
//! // Create from seconds (useful for migration)
//! let time = FrameTime::from_seconds(41.133333, 30.0);
//! assert_eq!(time.frames(), 1234);
//! ```

use serde::{Deserialize, Serialize};
use std::fmt;

/// Frame-based time representation.
///
/// Stores time as an integer frame count with a floating-point FPS.
/// All calculations are performed using integer frame counts to avoid
/// floating-point precision issues.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct FrameTime {
    /// Frame count (0-based)
    frames: i64,
    /// Frames per second (can be fractional, e.g., 29.97, 23.976)
    fps: f64,
}

impl FrameTime {
    /// Creates a new `FrameTime` from frame count and FPS.
    ///
    /// If `fps <= 0.0` or `fps.is_nan()`, defaults to 30.0 FPS.
    pub fn from_frames(frames: i64, fps: f64) -> Self {
        let fps = if fps <= 0.0 || !fps.is_finite() {
            30.0
        } else {
            fps
        };
        Self { frames, fps }
    }

    /// Creates a new `FrameTime` from seconds and FPS.
    ///
    /// This is useful for migrating from existing time representations.
    /// The frame count is calculated as `(seconds * fps).round() as i64`
    /// to minimize precision loss.
    ///
    /// If `fps <= 0.0` or `fps.is_nan()`, defaults to 30.0 FPS.
    pub fn from_seconds(seconds: f64, fps: f64) -> Self {
        let fps = if fps <= 0.0 || !fps.is_finite() {
            30.0
        } else {
            fps
        };
        let frames = (seconds * fps).round() as i64;
        Self { frames, fps }
    }

    /// Returns the frame count.
    pub fn frames(&self) -> i64 {
        self.frames
    }

    /// Returns the FPS (frames per second).
    pub fn fps(&self) -> f64 {
        self.fps
    }

    /// Returns the time in seconds as a floating-point value.
    ///
    /// Note: This may have precision limitations for fractional FPS values.
    /// For display purposes, use `format_sec_frame()` or `format_hms()` instead.
    pub fn seconds(&self) -> f64 {
        self.frames as f64 / self.fps
    }

    /// Formats the time as "seconds.frame" with fixed two-digit frame number.
    ///
    /// The frame number is displayed as a zero-padded two-digit number
    /// representing the frame within the current second.
    ///
    /// # Examples
    ///
    /// - `123.04` = 123 seconds, frame 4 (at 30 FPS, frame 4 = 0.133 seconds)
    /// - `5.29` = 5 seconds, frame 29 (at 30 FPS, last frame of that second)
    pub fn format_sec_frame(&self) -> String {
        let total_seconds = self.frames as f64 / self.fps;
        let seconds = total_seconds.floor() as i64;
        // For fractional FPS, use ceil of fps for modulo operation
        let fps_ceil = self.fps.ceil() as i64;
        // Ensure fps_ceil > 0
        let frames_in_second = if fps_ceil == 0 {
            0
        } else {
            self.frames % fps_ceil
        };
        // Handle negative frames
        let frames_in_second = if frames_in_second < 0 {
            // This shouldn't happen in practice
            0
        } else {
            frames_in_second
        };
        format!("{}.{:02}", seconds, frames_in_second)
    }

    /// Formats the time as "HH:MM:SS" (hours, minutes, seconds).
    ///
    /// This displays whole seconds only, without frame information.
    /// Useful for human-readable time displays.
    pub fn format_hms(&self) -> String {
        let total_seconds = self.seconds();
        let hours = (total_seconds / 3600.0) as i64;
        let minutes = ((total_seconds % 3600.0) / 60.0) as i64;
        let seconds = (total_seconds % 60.0) as i64;

        format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
    }

    /// Formats the time as "HH:MM:SS.FF" (hours, minutes, seconds, frames).
    ///
    /// Displays full time with frame information. Frames are shown as
    /// zero-padded two-digit numbers.
    pub fn format_hms_frame(&self) -> String {
        let total_seconds = self.seconds();
        let hours = (total_seconds / 3600.0) as i64;
        let minutes = ((total_seconds % 3600.0) / 60.0) as i64;
        let seconds = (total_seconds % 60.0) as i64;
        // For fractional FPS, use ceil of fps for modulo operation
        let fps_ceil = self.fps.ceil() as i64;
        let frames_in_second = if fps_ceil == 0 {
            0
        } else {
            self.frames % fps_ceil
        };
        let frames_in_second = if frames_in_second < 0 {
            0
        } else {
            frames_in_second
        };

        format!(
            "{:02}:{:02}:{:02}.{:02}",
            hours, minutes, seconds, frames_in_second
        )
    }

    /// Adds frames to this time, returning a new `FrameTime`.
    ///
    /// # Panics
    ///
    /// Panics if the FPS doesn't match.
    pub fn add_frames(&self, frames: i64) -> Self {
        Self {
            frames: self.frames + frames,
            fps: self.fps,
        }
    }

    /// Subtracts frames from this time, returning a new `FrameTime`.
    ///
    /// # Panics
    ///
    /// Panics if the FPS doesn't match or if the result would be negative.
    pub fn sub_frames(&self, frames: i64) -> Self {
        assert!(
            self.frames >= frames,
            "Cannot subtract more frames than available"
        );
        Self {
            frames: self.frames - frames,
            fps: self.fps,
        }
    }

    /// Adds seconds to this time, returning a new `FrameTime`.
    ///
    /// # Panics
    ///
    /// Panics if the FPS doesn't match.
    pub fn add_seconds(&self, seconds: f64) -> Self {
        let frames_to_add = (seconds * self.fps).round() as i64;
        self.add_frames(frames_to_add)
    }

    /// Subtracts seconds from this time, returning a new `FrameTime`.
    ///
    /// # Panics
    ///
    /// Panics if the FPS doesn't match or if the result would be negative.
    pub fn sub_seconds(&self, seconds: f64) -> Self {
        let frames_to_sub = (seconds * self.fps).round() as i64;
        self.sub_frames(frames_to_sub)
    }

    /// Returns the duration between two `FrameTime` instances.
    ///
    /// # Panics
    ///
    /// Panics if the FPS values don't match.
    pub fn duration(&self, other: &FrameTime) -> FrameDuration {
        assert!(
            (self.fps - other.fps).abs() < f64::EPSILON * 2.0,
            "FPS mismatch: {} != {}",
            self.fps,
            other.fps
        );

        let frame_diff = (self.frames - other.frames).abs();
        FrameDuration::from_frames(frame_diff, self.fps)
    }
}

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

/// Duration between two frame times.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FrameDuration {
    frames: i64,
    fps: f64,
}

impl FrameDuration {
    /// Creates a duration from frame count and FPS.
    /// If `fps <= 0.0` or `fps.is_nan()`, defaults to 30.0 FPS.
    pub fn from_frames(frames: i64, fps: f64) -> Self {
        let fps = if fps <= 0.0 || !fps.is_finite() {
            30.0
        } else {
            fps
        };
        Self { frames, fps }
    }

    /// Creates a duration from seconds and FPS.
    /// If `fps <= 0.0` or `fps.is_nan()`, defaults to 30.0 FPS.
    pub fn from_seconds(seconds: f64, fps: f64) -> Self {
        let fps = if fps <= 0.0 || !fps.is_finite() {
            30.0
        } else {
            fps
        };
        let frames = (seconds * fps).round() as i64;
        Self { frames, fps }
    }

    /// Returns the duration in frames.
    pub fn frames(&self) -> i64 {
        self.frames
    }

    /// Returns the duration in seconds.
    pub fn seconds(&self) -> f64 {
        self.frames as f64 / self.fps
    }

    /// Formats the duration as "seconds.frame" (same as `FrameTime`).
    pub fn format_sec_frame(&self) -> String {
        let temp_time = FrameTime::from_frames(self.frames, self.fps);
        temp_time.format_sec_frame()
    }

    /// Formats the duration as "HH:MM:SS".
    pub fn format_hms(&self) -> String {
        let temp_time = FrameTime::from_frames(self.frames, self.fps);
        temp_time.format_hms()
    }
}

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

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

    #[test]
    fn test_from_frames() {
        let time = FrameTime::from_frames(150, 30.0);
        assert_eq!(time.frames(), 150);
        assert_eq!(time.fps(), 30.0);
        assert_eq!(time.seconds(), 5.0);
    }

    #[test]
    fn test_from_seconds() {
        let time = FrameTime::from_seconds(5.0, 30.0);
        assert_eq!(time.frames(), 150);
        assert_eq!(time.seconds(), 5.0);
    }

    #[test]
    fn test_format_sec_frame() {
        let time = FrameTime::from_frames(123, 30.0);
        assert_eq!(time.format_sec_frame(), "4.03");

        let time = FrameTime::from_frames(29, 30.0);
        assert_eq!(time.format_sec_frame(), "0.29");

        let time = FrameTime::from_frames(60, 30.0);
        assert_eq!(time.format_sec_frame(), "2.00");
    }

    #[test]
    fn test_format_sec_frame_fractional_fps() {
        // 29.97 fps (NTSC)
        let time = FrameTime::from_frames(30, 29.97);
        // 30 frames at 29.97 fps = 1.001 seconds = 1 second, frame 0
        assert_eq!(time.format_sec_frame(), "1.00");

        let time = FrameTime::from_frames(60, 29.97);
        // 60 frames at 29.97 fps = 2.002 seconds = 2 seconds, frame 0
        assert_eq!(time.format_sec_frame(), "2.00");
    }

    #[test]
    fn test_format_hms() {
        let time = FrameTime::from_frames(3661, 30.0); // 122.033 seconds = 2 minutes 2 seconds
        assert_eq!(time.format_hms(), "00:02:02");

        let time = FrameTime::from_frames(4500, 30.0); // 150 seconds = 2 minutes 30 seconds
        assert_eq!(time.format_hms(), "00:02:30");
    }

    #[test]
    fn test_format_hms_frame() {
        let time = FrameTime::from_frames(123, 30.0); // 4 seconds, 3 frames
        assert_eq!(time.format_hms_frame(), "00:00:04.03");
    }

    #[test]
    fn test_add_sub_frames() {
        let time = FrameTime::from_frames(100, 30.0);
        let new_time = time.add_frames(50);
        assert_eq!(new_time.frames(), 150);

        let new_time = time.sub_frames(30);
        assert_eq!(new_time.frames(), 70);
    }

    #[test]
    fn test_add_sub_seconds() {
        let time = FrameTime::from_frames(100, 30.0);
        let new_time = time.add_seconds(2.0);
        assert_eq!(new_time.frames(), 160); // 100 + 60

        let new_time = time.sub_seconds(1.0);
        assert_eq!(new_time.frames(), 70); // 100 - 30
    }

    #[test]
    fn test_duration() {
        let time1 = FrameTime::from_frames(200, 30.0);
        let time2 = FrameTime::from_frames(150, 30.0);
        let duration = time1.duration(&time2);
        assert_eq!(duration.frames(), 50);
        assert_eq!(duration.seconds(), 50.0 / 30.0);
    }

    #[test]
    fn test_frame_duration() {
        let duration = FrameDuration::from_frames(90, 30.0);
        assert_eq!(duration.seconds(), 3.0);
        assert_eq!(duration.format_sec_frame(), "3.00");
        assert_eq!(duration.format_hms(), "00:00:03");
    }
}
