use chrono::{Datelike, NaiveDateTime, Timelike};
use crate::{context::HostHooks, value::IntegerOrNan};
pub(super) const MAX_TIMESTAMP: i64 = 864 * 10i64.pow(13);
pub(super) const MILLIS_PER_SECOND: i64 = 1000;
pub(super) const MILLIS_PER_MINUTE: i64 = MILLIS_PER_SECOND * 60;
pub(super) const MILLIS_PER_HOUR: i64 = MILLIS_PER_MINUTE * 60;
pub(super) const MILLIS_PER_DAY: i64 = MILLIS_PER_HOUR * 24;
pub(super) const MIN_YEAR: i64 = -300_000;
pub(super) const MAX_YEAR: i64 = -MIN_YEAR;
pub(super) const MIN_MONTH: i64 = MIN_YEAR * 12;
pub(super) const MAX_MONTH: i64 = MAX_YEAR * 12;
pub(super) const fn day_from_year(year: i64) -> i64 {
const YEAR_DELTA: i64 = 399_999;
const fn day(year: i64) -> i64 {
let year = year + YEAR_DELTA;
365 * year + year / 4 - year / 100 + year / 400
}
assert!(MIN_YEAR <= year && year <= MAX_YEAR);
day(year) - day(1970)
}
pub(super) fn make_time(hour: i64, min: i64, sec: i64, ms: i64) -> Option<i64> {
let h_ms = hour.checked_mul(MILLIS_PER_HOUR)?;
let m_ms = min.checked_mul(MILLIS_PER_MINUTE)?;
let s_ms = sec.checked_mul(MILLIS_PER_SECOND)?;
h_ms.checked_add(m_ms)?.checked_add(s_ms)?.checked_add(ms)
}
pub(super) fn make_day(mut year: i64, mut month: i64, date: i64) -> Option<i64> {
if !(MIN_YEAR..=MAX_YEAR).contains(&year) || !(MIN_MONTH..=MAX_MONTH).contains(&month) {
return None;
}
year += month / 12;
month %= 12;
if month < 0 {
month += 12;
year -= 1;
}
let month = usize::try_from(month).expect("month must be between 0 and 11 at this point");
let mut day = day_from_year(year);
if (year % 4 != 0) || (year % 100 == 0 && year % 400 != 0) {
day += [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334][month];
} else {
day += [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335][month];
}
(day - 1).checked_add(date)
}
pub(super) fn make_date(day: i64, time: i64) -> Option<i64> {
day.checked_mul(MILLIS_PER_DAY)?.checked_add(time)
}
pub(super) fn time_clip(time: i64) -> Option<i64> {
(time.checked_abs()? <= MAX_TIMESTAMP).then_some(time)
}
#[derive(Default, Debug, Clone, Copy)]
pub(super) struct DateParameters {
pub(super) year: Option<IntegerOrNan>,
pub(super) month: Option<IntegerOrNan>,
pub(super) date: Option<IntegerOrNan>,
pub(super) hour: Option<IntegerOrNan>,
pub(super) minute: Option<IntegerOrNan>,
pub(super) second: Option<IntegerOrNan>,
pub(super) millisecond: Option<IntegerOrNan>,
}
pub(super) fn replace_params<const LOCAL: bool>(
datetime: i64,
params: DateParameters,
hooks: &dyn HostHooks,
) -> Option<i64> {
let datetime = NaiveDateTime::from_timestamp_millis(datetime)?;
let DateParameters {
year,
month,
date,
hour,
minute,
second,
millisecond,
} = params;
let datetime = if LOCAL {
hooks.local_from_utc(datetime).naive_local()
} else {
datetime
};
let year = match year {
Some(i) => i.as_integer()?,
None => i64::from(datetime.year()),
};
let month = match month {
Some(i) => i.as_integer()?,
None => i64::from(datetime.month() - 1),
};
let date = match date {
Some(i) => i.as_integer()?,
None => i64::from(datetime.day()),
};
let hour = match hour {
Some(i) => i.as_integer()?,
None => i64::from(datetime.hour()),
};
let minute = match minute {
Some(i) => i.as_integer()?,
None => i64::from(datetime.minute()),
};
let second = match second {
Some(i) => i.as_integer()?,
None => i64::from(datetime.second()),
};
let millisecond = match millisecond {
Some(i) => i.as_integer()?,
None => i64::from(datetime.timestamp_subsec_millis()),
};
let new_day = make_day(year, month, date)?;
let new_time = make_time(hour, minute, second, millisecond)?;
let mut ts = make_date(new_day, new_time)?;
if LOCAL {
ts = hooks
.local_from_naive_local(NaiveDateTime::from_timestamp_millis(ts)?)
.earliest()?
.naive_utc()
.timestamp_millis();
}
time_clip(ts)
}