///|
/// Gregorian calendar arithmetic.
///
/// The conversions between a year/month/day triple and a day count relative to
/// the Unix epoch use Neri and Schneider's "Euclidean affine functions and
/// their application to calendar algorithms", the same branch-free formulation
/// `temporal_rs` uses. Temporal's supported year range (`-271821 ..= 275760`)
/// is far wider than the shift window given in the paper, so the epoch is
/// re-centred with an extended shift constant.

///|
/// The computational Rata Die of the Unix epoch.
const EPOCH_COMPUTATIONAL_RATA_DIE : Int64 = 719_468L

///|
/// Days in a full 400-year Gregorian cycle.
const DAYS_IN_A_400Y_CYCLE : Int64 = 146_097L

///|
/// Cycle shift used when converting epoch days back to a date. Chosen so the
/// epoch sits in the middle of the shifted window.
const SHIFT_CONSTANT : Int64 = 3670L

///|
/// Cycle shift used when converting a date to epoch days. Larger than
/// [`SHIFT_CONSTANT`] because the intermediate year may be arbitrarily far
/// outside the supported range before balancing.
const SHIFT_CONSTANT_EXTENDED : Int64 = 5_368_710L

///|
/// Floor division for `Int64`, rounding toward negative infinity.
fn div_euclid(dividend : Int64, divisor : Int64) -> Int64 {
  let q = dividend / divisor
  if dividend % divisor != 0L && (dividend < 0L) != (divisor < 0L) {
    q - 1L
  } else {
    q
  }
}

///|
/// Non-negative remainder for `Int64`.
fn rem_euclid(dividend : Int64, divisor : Int64) -> Int64 {
  let r = dividend % divisor
  if r < 0L {
    r + divisor.abs()
  } else {
    r
  }
}

///|
/// Floor division and non-negative remainder in one step.
fn div_mod(dividend : Int64, divisor : Int64) -> (Int64, Int64) {
  (div_euclid(dividend, divisor), rem_euclid(dividend, divisor))
}

///|
/// Floor division for `Int`, rounding toward negative infinity.
fn div_euclid_i(dividend : Int, divisor : Int) -> Int {
  let q = dividend / divisor
  if dividend % divisor != 0 && (dividend < 0) != (divisor < 0) {
    q - 1
  } else {
    q
  }
}

///|
/// Non-negative remainder for `Int`.
fn rem_euclid_i(dividend : Int, divisor : Int) -> Int {
  let r = dividend % divisor
  if r < 0 {
    r + divisor.abs()
  } else {
    r
  }
}

///|
/// Returns whether `year` is a leap year in the proleptic Gregorian calendar.
///
/// ```mbt check
/// test {
///   inspect(@temporal.is_leap_year(2024), content="true")
///   inspect(@temporal.is_leap_year(1900), content="false")
///   inspect(@temporal.is_leap_year(2000), content="true")
/// }
/// ```
pub fn is_leap_year(year : Int) -> Bool {
  if year % 4 != 0 {
    false
  } else if year % 100 != 0 {
    true
  } else {
    year % 400 == 0
  }
}

///|
/// Returns the number of days in the given ISO month.
///
/// `month` is 1-based.
///
/// ```mbt check
/// test {
///   inspect(@temporal.iso_days_in_month(2024, 2), content="29")
///   inspect(@temporal.iso_days_in_month(2023, 2), content="28")
///   inspect(@temporal.iso_days_in_month(2023, 12), content="31")
/// }
/// ```
pub fn iso_days_in_month(year : Int, month : Int) -> Int {
  match month {
    1 | 3 | 5 | 7 | 8 | 10 | 12 => 31
    4 | 6 | 9 | 11 => 30
    2 => if is_leap_year(year) { 29 } else { 28 }
    _ => 0
  }
}

///|
/// Returns the number of days in the given ISO year.
pub fn iso_days_in_year(year : Int) -> Int {
  if is_leap_year(year) {
    366
  } else {
    365
  }
}

///|
/// Converts a Gregorian year/month/day to days since 1970-01-01.
///
/// `month` is 1-based. The date need not be valid: an out-of-range day is
/// simply carried, which is what the balancing operations rely on.
///
/// ```mbt check
/// test {
///   inspect(@temporal.epoch_days_from_gregorian_date(1970, 1, 1), content="0")
///   inspect(@temporal.epoch_days_from_gregorian_date(1969, 12, 31), content="-1")
///   inspect(
///     @temporal.epoch_days_from_gregorian_date(275760, 9, 14),
///     content="100000001",
///   )
/// }
/// ```
pub fn epoch_days_from_gregorian_date(
  year : Int,
  month : Int,
  day : Int,
) -> Int64 {
  let shift = SHIFT_CONSTANT_EXTENDED * DAYS_IN_A_400Y_CYCLE +
    EPOCH_COMPUTATIONAL_RATA_DIE
  // Shift January and February into the previous computational year so that
  // the leap day lands at the end of the cycle.
  let j = if month <= 2 { 1L } else { 0L }
  let computational_year = year.to_int64() + 400L * SHIFT_CONSTANT_EXTENDED - j
  let computational_month = month.to_int64() + 12L * j
  let computational_day = day.to_int64() - 1L
  let century = computational_year / 100L
  let y_star = 1461L * computational_year / 4L - century + century / 4L
  let m_star = (979L * computational_month - 2919L) / 32L
  y_star + m_star + computational_day - shift
}

///|
/// Converts days since 1970-01-01 to a Gregorian year/month/day triple.
///
/// ```mbt check
/// test {
///   debug_inspect(@temporal.ymd_from_epoch_days(0), content="(1970, 1, 1)")
///   debug_inspect(
///     @temporal.ymd_from_epoch_days(-100000001),
///     content="(-271821, 4, 19)",
///   )
/// }
/// ```
pub fn ymd_from_epoch_days(epoch_days : Int64) -> (Int, Int, Int) {
  let rata_die = epoch_days +
    EPOCH_COMPUTATIONAL_RATA_DIE +
    DAYS_IN_A_400Y_CYCLE * SHIFT_CONSTANT
  let year_shift = 400L * SHIFT_CONSTANT
  let (year, month, day) = gregorian_ymd(rata_die)
  ((year - year_shift).to_int(), month.to_int(), day.to_int())
}

///|
/// Neri-Schneider's third set of equations: computational Rata Die to a
/// year/month/day triple in the shifted computational calendar.
fn gregorian_ymd(rata_die : Int64) -> (Int64, Int64, Int64) {
  let n1 = 4L * rata_die + 3L
  let century = div_euclid(n1, DAYS_IN_A_400Y_CYCLE)
  let century_rem = rem_euclid(n1, DAYS_IN_A_400Y_CYCLE)
  let n2 = century_rem | 3L
  let p2 = 2_939_745L * n2
  let year_of_century = p2 / 4_294_967_296L
  let day_of_year = p2 % 4_294_967_296L / 2_939_745L / 4L
  let year = 100L * century + year_of_century
  let n3 = 2141L * day_of_year + 197_913L
  let month = n3 / 65_536L
  let day = n3 % 65_536L / 2141L
  // Undo the January/February shift applied on the way in.
  let j = if day_of_year >= 306L { 1L } else { 0L }
  (year + j, month - 12L * j, day + 1L)
}

///|
/// Converts epoch milliseconds to a Gregorian year/month/day triple.
pub fn ymd_from_epoch_milliseconds(epoch_ms : Int64) -> (Int, Int, Int) {
  ymd_from_epoch_days(div_euclid(epoch_ms, MS_PER_DAY))
}

///|
/// `EpochDaysToEpochMS`: combines a day count and a within-day millisecond
/// offset into epoch milliseconds.
fn epoch_days_to_epoch_ms(day : Int64, time : Int64) -> Int64 {
  day * MS_PER_DAY + time
}

///|
/// Returns the ISO day of the week, Monday = 1 through Sunday = 7.
///
/// ```mbt check
/// test {
///   // 1970-01-01 was a Thursday.
///   inspect(@temporal.iso_day_of_week(1970, 1, 1), content="4")
/// }
/// ```
pub fn iso_day_of_week(year : Int, month : Int, day : Int) -> Int {
  let epoch_days = epoch_days_from_gregorian_date(year, month, day)
  // 1970-01-01 was a Thursday, ISO weekday 4.
  (rem_euclid(epoch_days + 3L, 7L) + 1L).to_int()
}

///|
/// Returns the 1-based day of the year.
///
/// ```mbt check
/// test {
///   inspect(@temporal.iso_day_of_year(2024, 3, 1), content="61")
/// }
/// ```
pub fn iso_day_of_year(year : Int, month : Int, day : Int) -> Int {
  let start = epoch_days_from_gregorian_date(year, 1, 1)
  let current = epoch_days_from_gregorian_date(year, month, day)
  (current - start + 1L).to_int()
}

///|
/// Returns the ISO 8601 week-of-year number and the year that week belongs to.
///
/// ISO weeks start on Monday, and week 1 is the week containing the first
/// Thursday of the year, so days at either end of a year can belong to a week
/// of the adjacent year.
///
/// ```mbt check
/// test {
///   // 2021-01-01 was a Friday, falling in week 53 of 2020.
///   debug_inspect(@temporal.iso_week_of_year(2021, 1, 1), content="(53, 2020)")
///   debug_inspect(@temporal.iso_week_of_year(2024, 1, 1), content="(1, 2024)")
/// }
/// ```
pub fn iso_week_of_year(year : Int, month : Int, day : Int) -> (Int, Int) {
  let day_of_week = iso_day_of_week(year, month, day)
  let day_of_year = iso_day_of_year(year, month, day)
  // The Thursday of this week determines which year the week belongs to.
  let week = (day_of_year - day_of_week + 10) / 7
  if week < 1 {
    // Belongs to the last week of the previous year.
    let prev = year - 1
    let jan1_dow = iso_day_of_week(prev, 1, 1)
    let weeks = if jan1_dow == 4 || (is_leap_year(prev) && jan1_dow == 3) {
      53
    } else {
      52
    }
    (weeks, prev)
  } else if week > 52 {
    // Only years starting on Thursday, or leap years starting on Wednesday,
    // have a 53rd week.
    let jan1_dow = iso_day_of_week(year, 1, 1)
    if jan1_dow == 4 || (is_leap_year(year) && jan1_dow == 3) {
      (53, year)
    } else {
      (1, year + 1)
    }
  } else {
    (week, year)
  }
}