1use std::thread;
2use std::time::Duration;
3use time::{Date, Month, Weekday};
4
5/// Read the time since the Unix epoch in nanoseconds. Will return 0 if the system-reported time is
6/// somehow before the Unix epoch.
7///
8/// Uses `SystemTime` on native platforms and `Date` in WASM.
9#[must_use]
10pub fn current_time_nanos() -> u128 {
11    cfg_select! {
12        target_arch = "wasm32" => {
13            let current_time_ms = js_sys::Date::now();
14            (current_time_ms * 1_000_000.0) as u128
15        }
16        _ => {
17            use std::time::SystemTime;
18
19            SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_nanos()
20        }
21    }
22}
23
24/// Sleep until at least the specified time. Returns the current time in nanoseconds after sleeping.
25///
26/// This implementation will try to sleep until 1ms before the target time and then it will busy
27/// wait until the target time. This is to work around the fact that `thread::sleep()` only guarantees
28/// that it will sleep _at least_ the specified duration, and it could sleep longer. Sleeping longer
29/// seems to be particularly common on Windows.
30///
31/// If the current time is already past the target time, this function will return immediately
32/// without sleeping.
33#[inline]
34#[allow(clippy::must_use_candidate)]
35pub fn sleep_until(time_nanos: u128) -> u128 {
36    loop {
37        let now = current_time_nanos();
38        if now >= time_nanos {
39            return now;
40        }
41
42        let duration = Duration::from_nanos((time_nanos - now) as u64);
43        if duration > Duration::from_millis(1) {
44            thread::sleep(duration - Duration::from_millis(1));
45        }
46    }
47}
48
49/// Determine the number of days in the given month+year.
50///
51/// Leap years are accounted for, but only in that February is assumed to be 29 days in every 4th
52/// year. Other leap year rules are intentionally not applied.
53#[must_use]
54pub fn days_in_month(month: u8, year: u8) -> u8 {
55    match month {
56        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
57        4 | 6 | 9 | 11 => 30,
58        2 => {
59            // This is not strictly accurate (it doesn't account for not divisible by 100 or divisible
60            // by 400) but it matches what the RTC chips do
61            if year.is_multiple_of(4) { 29 } else { 28 }
62        }
63        _ => {
64            log::error!("Invalid month: {month}, defaulting to 31 days in month");
65            31
66        }
67    }
68}
69
70/// Determine the weekday of a given date. Day and month should both start at 1, not 0.
71#[must_use]
72pub fn day_of_week(day: u8, month: u8, year: u16) -> Weekday {
73    match Date::from_calendar_date(year.into(), convert_month(month), day) {
74        Ok(date) => date.weekday(),
75        Err(err) => {
76            log::error!("Invalid date (day={day}, month={month}, year={year}): {err}");
77            Weekday::Sunday
78        }
79    }
80}
81
82fn convert_month(month: u8) -> Month {
83    use Month::*;
84
85    match month {
86        1 => January,
87        2 => February,
88        3 => March,
89        4 => April,
90        5 => May,
91        6 => June,
92        7 => July,
93        8 => August,
94        9 => September,
95        10 => October,
96        11 => November,
97        12 => December,
98        _ => {
99            log::error!("Invalid month: {month} defaulting to January");
100            January
101        }
102    }
103}