//! Time handling structures and functions
use core::{
    ops::{Add, Sub},
    time::Duration,
};

use abi_lib::SyscallRegister;

/// Number of nanoseconds in each second
pub const NANOSECONDS_IN_SECOND: u64 = 1_000_000_000;
/// Number of microseconds in each second
pub const MICROSECONDS_IN_SECOND: u64 = 1_000_000;
/// Number of milliseconds in each second
pub const MILLISECONDS_IN_SECOND: u64 = 1_000;

/// Represents a point of time as measured by some system clock
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub struct SystemTime {
    seconds: u64,
    nanoseconds: u64,
}

/// Represents a certain clock type
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ClockType {
    /// Monotonically increasing clock since some arbitrary point in the past
    Monotonic,
    /// "Real-world" clock, subject to jumps
    RealTime,
}

impl SyscallRegister for ClockType {
    fn into_syscall_register(self) -> usize {
        match self {
            Self::Monotonic => 1,
            Self::RealTime => 2,
        }
    }

    fn from_syscall_register(value: usize) -> Self {
        match value {
            1 => Self::Monotonic,
            2 => Self::RealTime,
            _ => unreachable!(),
        }
    }
}

impl SystemTime {
    /// Zero time, also represents UNIX Epoch for real-world clocks
    pub const ZERO: Self = Self {
        seconds: 0,
        nanoseconds: 0,
    };

    /// Constructs a new [SystemTime] from seconds and nanoseconds, without performing
    /// any adjustments.
    ///
    /// # Safety
    ///
    /// The caller must guarantee the `nanoseconds` value does not violate the invariant that
    /// `nanoseconds` is less than [NANOSECONDS_IN_SECOND].
    pub unsafe fn new_unchecked(seconds: u64, nanoseconds: u64) -> Self {
        Self {
            seconds,
            nanoseconds,
        }
    }

    /// Constructs a new [SystemTime] from seconds and nanoseconds. If `nanoseconds` contains extra
    /// full seconds, those are converted to seconds (i.e. the time is "normalized").
    ///
    /// # Panics
    ///
    /// This function will panic if the conversion result overflows [u64] in the `seconds` field.
    pub fn new(seconds: u64, nanoseconds: u64) -> Self {
        Self::new_opt(seconds, nanoseconds).unwrap()
    }

    /// Constructs a new [SystemTime] from seconds and nanoseconds. This function returns `None` if
    /// conversion fails. See [SystemTime::new].
    pub fn new_opt(mut seconds: u64, nanoseconds: u64) -> Option<Self> {
        seconds = seconds.checked_add(nanoseconds / NANOSECONDS_IN_SECOND)?;
        Some(Self {
            seconds,
            nanoseconds: nanoseconds % NANOSECONDS_IN_SECOND,
        })
    }

    /// Constructs a [SystemTime] from seconds part only
    pub fn from_seconds(seconds: u64) -> Self {
        Self {
            seconds,
            nanoseconds: 0,
        }
    }

    /// Returns the number of full milliseconds represented by this structure
    pub fn as_millis(&self) -> u128 {
        (self.seconds as u128 * MILLISECONDS_IN_SECOND as u128)
            + (self.nanoseconds as u128 / 1000000)
    }

    /// Returns the number of sub-second nanoseconds
    #[inline(always)]
    pub fn subsec_nanos(&self) -> u64 {
        self.nanoseconds
    }

    /// Returns the number of full seconds
    #[inline(always)]
    pub fn seconds(&self) -> u64 {
        self.seconds
    }

    /// Adds a [Duration] to this [SystemTime], returns `None` if the addition overflows
    pub fn checked_add_duration(&self, other: &Duration) -> Option<Self> {
        let nanoseconds = self.nanoseconds.checked_add(other.subsec_nanos() as u64)?;
        let seconds = nanoseconds / NANOSECONDS_IN_SECOND;
        let nanoseconds = nanoseconds % NANOSECONDS_IN_SECOND;
        let seconds = self
            .seconds
            .checked_add(other.as_secs())?
            .checked_add(seconds)?;

        Some(Self {
            seconds,
            nanoseconds,
        })
    }

    /// Subtracts a [Duration] from this [SystemTime], returns `None` if the subtraction underflows
    pub fn checked_sub_duration(&self, other: &Duration) -> Option<Self> {
        let mut seconds = self.seconds.checked_sub(other.as_secs())?;
        let nanoseconds = if self.nanoseconds < other.subsec_nanos() as u64 {
            seconds = seconds.checked_sub(1)?;
            NANOSECONDS_IN_SECOND + self.nanoseconds - other.subsec_nanos() as u64
        } else {
            self.nanoseconds - other.subsec_nanos() as u64
        };

        Some(Self {
            seconds,
            nanoseconds,
        })
    }

    /// Subtracts two [SystemTime]s, returns `Ok(self - other)` if `self >= other`,
    /// `Err(other - self)` otherwise.
    pub fn sub_time(&self, other: &SystemTime) -> Result<Duration, Duration> {
        if self >= other {
            Ok(self.checked_sub_time(other).unwrap())
        } else {
            match other.sub_time(self) {
                Ok(d) => Err(d),
                Err(d) => Ok(d),
            }
        }
    }

    /// Subtracts two [SystemTime]s, returns `None` if the subtraction underflows
    pub fn checked_sub_time(&self, other: &SystemTime) -> Option<Duration> {
        let mut seconds = self.seconds.checked_sub(other.seconds)?;
        let nanoseconds = if self.nanoseconds < other.nanoseconds {
            seconds = seconds.checked_sub(1)?;
            NANOSECONDS_IN_SECOND + self.nanoseconds - other.nanoseconds
        } else {
            self.nanoseconds - other.nanoseconds
        };

        Some(Duration::new(seconds, nanoseconds as u32))
    }
}

impl Add<Duration> for SystemTime {
    type Output = SystemTime;

    #[inline]
    fn add(self, rhs: Duration) -> Self::Output {
        self.checked_add_duration(&rhs).unwrap()
    }
}

impl Sub<Duration> for SystemTime {
    type Output = SystemTime;

    #[inline]
    fn sub(self, rhs: Duration) -> Self::Output {
        self.checked_sub_duration(&rhs).unwrap()
    }
}

#[cfg(test)]
mod tests {
    use core::time::Duration;

    use super::{SystemTime, NANOSECONDS_IN_SECOND};

    #[test]
    fn time_sub_time() {
        let t0 = SystemTime::new(1, NANOSECONDS_IN_SECOND - 1);
        let t1 = SystemTime::new(2, 0);

        assert_eq!(t0.sub_time(&t1).unwrap_err(), Duration::new(0, 1));
        assert_eq!(t1.sub_time(&t0).unwrap(), Duration::new(0, 1));
    }

    #[test]
    fn time_checked_sub_time() {
        let t0 = SystemTime::new(1, NANOSECONDS_IN_SECOND - 1);
        let t1 = SystemTime::new(2, 0);

        assert!(t0.checked_sub_time(&t1).is_none());
        assert_eq!(t1.checked_sub_time(&t0).unwrap(), Duration::new(0, 1));
    }

    #[test]
    fn time_sub_duration() {
        let t0 = SystemTime::new(2, 1);

        assert_eq!(
            t0.checked_sub_duration(&Duration::new(1, 1)).unwrap(),
            SystemTime::new(1, 0)
        );
        assert_eq!(
            t0.checked_sub_duration(&Duration::new(1, 2)).unwrap(),
            SystemTime::new(0, NANOSECONDS_IN_SECOND - 1)
        );
        assert_eq!(
            t0.checked_sub_duration(&Duration::new(1, NANOSECONDS_IN_SECOND as u32 - 1))
                .unwrap(),
            SystemTime::new(0, 2)
        );
        assert_eq!(
            t0.checked_sub_duration(&Duration::new(2, 1)).unwrap(),
            SystemTime::ZERO
        );
        assert!(t0.checked_sub_duration(&Duration::new(2, 2)).is_none());
    }

    #[test]
    fn time_add_duration() {
        let t0 = SystemTime::new(2, 1);
        let t1 = SystemTime::new(u64::MAX, 1);

        assert_eq!(
            t0.checked_add_duration(&Duration::new(1, 0)).unwrap(),
            SystemTime::new(3, 1)
        );
        assert_eq!(
            t0.checked_add_duration(&Duration::new(1, NANOSECONDS_IN_SECOND as u32 - 1))
                .unwrap(),
            SystemTime::new(4, 0)
        );
        assert_eq!(
            t1.checked_add_duration(&Duration::new(0, NANOSECONDS_IN_SECOND as u32 - 2))
                .unwrap(),
            SystemTime::new(u64::MAX, NANOSECONDS_IN_SECOND - 1)
        );
        assert!(t1
            .checked_add_duration(&Duration::new(0, NANOSECONDS_IN_SECOND as u32 - 1))
            .is_none());
        assert!(t1.checked_add_duration(&Duration::new(1, 0)).is_none());
    }
}
