//! Implementation of International Atomic Time (TAI).

use num_traits::ConstZero;

use crate::{
    Date, Duration, FromTimeScale, IntoTimeScale, Month, TimePoint,
    time_scale::{AbsoluteTimeScale, TerrestrialTime, TimeScale, datetime::UniformDateTimeScale},
};

pub type TaiTime = TimePoint<Tai>;

/// Time scale representing the International Atomic Time standard (TAI). TAI has no leap seconds
/// and increases monotonically at a constant rate. This makes it highly suitable for scientific
/// and high-accuracy timekeeping.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Tai;

impl TimeScale for Tai {
    const NAME: &'static str = "International Atomic Time";

    const ABBREVIATION: &'static str = "TAI";
}

impl AbsoluteTimeScale for Tai {
    const EPOCH: Date = match Date::from_historic_date(1958, Month::January, 1) {
        Ok(epoch) => epoch,
        Err(_) => unreachable!(),
    };
}

impl UniformDateTimeScale for Tai {}

impl<Scale: ?Sized> TimePoint<Scale> {
    pub fn from_tai(time_point: TaiTime) -> Self
    where
        Self: FromTimeScale<Tai>,
    {
        Self::from_time_scale(time_point)
    }

    pub fn into_tai(self) -> TaiTime
    where
        Self: IntoTimeScale<Tai>,
    {
        self.into_time_scale()
    }
}

impl TerrestrialTime for Tai {
    const TAI_OFFSET: Duration = Duration::ZERO;
}

/// Test function that verifies whether a given Gregorian date-time maps to the provided time since
/// epoch (in seconds). If not, panics.
#[cfg(test)]
fn check_gregorian_datetime(
    year: i32,
    month: Month,
    day: u8,
    hour: u8,
    minute: u8,
    second: u8,
    time_since_epoch: Duration,
) {
    assert_eq!(
        TaiTime::from_gregorian_datetime(year, month, day, hour, minute, second)
            .unwrap()
            .time_since_epoch(),
        time_since_epoch
    );
}

/// Verifies this implementation by computing the `TaiTime` for some known (computed manually or
/// obtained elsewhere) time stamps.
#[test]
fn known_timestamps() {
    use crate::Month::*;

    check_gregorian_datetime(1958, January, 1, 0, 0, 0, Duration::seconds(0));
    check_gregorian_datetime(1958, January, 2, 0, 0, 0, Duration::seconds(86400));
    check_gregorian_datetime(1960, January, 1, 0, 0, 0, Duration::seconds(63072000));
    check_gregorian_datetime(1961, January, 1, 0, 0, 0, Duration::seconds(94694400));
    check_gregorian_datetime(1970, January, 1, 0, 0, 0, Duration::seconds(378691200));
    check_gregorian_datetime(1976, January, 1, 0, 0, 0, Duration::seconds(567993600));
    check_gregorian_datetime(2025, July, 16, 16, 23, 24, Duration::seconds(2131374204));
    check_gregorian_datetime(2034, December, 26, 8, 2, 37, Duration::seconds(2429424157));
    check_gregorian_datetime(2760, April, 1, 21, 59, 58, Duration::seconds(25316575198));
    check_gregorian_datetime(1643, January, 4, 1, 1, 33, Duration::seconds(-9940143507));
}

#[cfg(test)]
fn gregorian_datetime_roundtrip(
    year: i32,
    month: Month,
    day: u8,
    hour: u8,
    minute: u8,
    second: u8,
) {
    use crate::GregorianDate;
    use crate::IntoDateTime;

    let time = TaiTime::from_gregorian_datetime(year, month, day, hour, minute, second).unwrap();
    let (date, hour2, minute2, second2) = time.into_datetime();
    let gregorian_date = GregorianDate::from_date(date);
    assert_eq!(gregorian_date.year(), year);
    assert_eq!(gregorian_date.month(), month);
    assert_eq!(gregorian_date.day(), day);
    assert_eq!(hour, hour2);
    assert_eq!(minute, minute2);
    assert_eq!(second, second2);
}

#[test]
fn date_decomposition() {
    gregorian_datetime_roundtrip(1999, Month::August, 22, 0, 0, 0);
    gregorian_datetime_roundtrip(1958, Month::January, 1, 0, 0, 0);
    gregorian_datetime_roundtrip(1958, Month::January, 2, 0, 0, 0);
    gregorian_datetime_roundtrip(1960, Month::January, 1, 0, 0, 0);
    gregorian_datetime_roundtrip(1961, Month::January, 1, 0, 0, 0);
    gregorian_datetime_roundtrip(1970, Month::January, 1, 0, 0, 0);
    gregorian_datetime_roundtrip(1976, Month::January, 1, 0, 0, 0);
    gregorian_datetime_roundtrip(2025, Month::July, 16, 16, 23, 24);
    gregorian_datetime_roundtrip(2034, Month::December, 26, 8, 2, 37);
    gregorian_datetime_roundtrip(2760, Month::April, 1, 21, 59, 58);
    gregorian_datetime_roundtrip(1643, Month::January, 4, 1, 1, 33);
    gregorian_datetime_roundtrip(1996, Month::January, 1, 3, 0, 0);
}
