///|
/// Minute based time handling for change windows.
///
/// An instant is the number of minutes since `1970-01-01T00:00` on a single,
/// unnamed time zone. Parsing only accepts `YYYY-MM-DDTHH:MM` (a space instead
/// of `T` is also accepted) for years 1970-2199, which keeps every instant
/// inside 32 bit `Int` range on all MoonBit backends.

///|
/// Lowest year accepted by `parse_instant`, the zero of the minute axis.
const MIN_YEAR : Int = 1970

///|
/// Highest year accepted by `parse_instant`.
const MAX_YEAR : Int = 2199

///|
/// Minutes in one day.
const MINUTES_PER_DAY : Int = 1440

///|
/// A calendar date on the minute axis, used to convert days back to text.
priv struct Civil {
  year : Int
  month : Int
  day : Int
}

///|
/// Gregorian leap year rule.
fn is_leap_year(year : Int) -> Bool {
  (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}

///|
/// Number of days in a month, `0` for a month outside 1-12.
fn 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
  }
}

///|
/// Days since 1970-01-01 for a Gregorian date.
///
/// Adapted from the public domain `days_from_civil` algorithm described by
/// Howard Hinnant in "chrono-Compatible Low-Level Date Algorithms". See
/// `SOURCES.md`. Division truncates towards zero, which is exact here because
/// every accepted year is positive.
fn days_from_civil(year : Int, month : Int, day : Int) -> Int {
  let adjusted_year = if month <= 2 { year - 1 } else { year }
  let era = adjusted_year / 400
  let year_of_era = adjusted_year - era * 400
  let month_index = (month + 9) % 12
  let day_of_year = (153 * month_index + 2) / 5 + day - 1
  let day_of_era = year_of_era * 365 +
    year_of_era / 4 -
    year_of_era / 100 +
    day_of_year
  era * 146097 + day_of_era - 719468
}

///|
/// Inverse of `days_from_civil`.
fn civil_from_days(days : Int) -> Civil {
  let shifted = days + 719468
  let era = shifted / 146097
  let day_of_era = shifted - era * 146097
  let year_of_era = (
      day_of_era - day_of_era / 1460 + day_of_era / 36524 - day_of_era / 146096
    ) /
    365
  let year = year_of_era + era * 400
  let day_of_year = day_of_era -
    (365 * year_of_era + year_of_era / 4 - year_of_era / 100)
  let month_index = (5 * day_of_year + 2) / 153
  let day = day_of_year - (153 * month_index + 2) / 5 + 1
  let month = if month_index < 10 { month_index + 3 } else { month_index - 9 }
  let shifted_year = if month <= 2 { year + 1 } else { year }
  { year: shifted_year, month, day, }
}

///|
/// Parse `YYYY-MM-DDTHH:MM` into minutes since 1970-01-01T00:00.
///
/// A single space is accepted where the `T` belongs. Everything else is
/// rejected: other lengths, separators, a year outside 1970-2199, an impossible
/// date such as `2026-02-30T10:00`, `24` as hour and `60` as minute.
pub fn parse_instant(text : String) -> Int? {
  let value = text.trim()
  if value.length() != 16 {
    return None
  }
  if value[4] != '-' || value[7] != '-' || value[13] != ':' {
    return None
  }
  let separator = value[10]
  if separator != 'T' && separator != ' ' {
    return None
  }
  let year = match parse_digits(value[0:4]) {
    Some(parsed) => parsed
    None => return None
  }
  let month = match parse_digits(value[5:7]) {
    Some(parsed) => parsed
    None => return None
  }
  let day = match parse_digits(value[8:10]) {
    Some(parsed) => parsed
    None => return None
  }
  let hour = match parse_digits(value[11:13]) {
    Some(parsed) => parsed
    None => return None
  }
  let minute = match parse_digits(value[14:16]) {
    Some(parsed) => parsed
    None => return None
  }
  if year < MIN_YEAR || year > MAX_YEAR {
    return None
  }
  if month < 1 || month > 12 {
    return None
  }
  if day < 1 || day > days_in_month(year, month) {
    return None
  }
  if hour > 23 || minute > 59 {
    return None
  }
  Some(days_from_civil(year, month, day) * MINUTES_PER_DAY + hour * 60 + minute)
}

///|
/// Render minutes since 1970-01-01T00:00 as `YYYY-MM-DDTHH:MM`.
///
/// This is the inverse of `parse_instant` for every instant it accepts.
/// Negative input is clamped to `1970-01-01T00:00`, because instants before the
/// epoch are outside the supported range.
pub fn format_instant(minutes : Int) -> String {
  let safe = if minutes < 0 { 0 } else { minutes }
  let days = safe / MINUTES_PER_DAY
  let rest = safe % MINUTES_PER_DAY
  let civil = civil_from_days(days)
  let builder = StringBuilder()
  builder.write_string(pad_left(civil.year, 4))
  builder.write_char('-')
  builder.write_string(pad_left(civil.month, 2))
  builder.write_char('-')
  builder.write_string(pad_left(civil.day, 2))
  builder.write_char('T')
  builder.write_string(pad_left(rest / 60, 2))
  builder.write_char(':')
  builder.write_string(pad_left(rest % 60, 2))
  builder.to_string()
}

///|
/// Render a duration in minutes as `45m`, `2h` or `1h30m`. The value is treated
/// as a magnitude, and `0` renders as `0m`.
pub fn format_duration(minutes : Int) -> String {
  let safe = if minutes < 0 { -minutes } else { minutes }
  let hours = safe / 60
  let rest = safe % 60
  if hours == 0 {
    rest.to_string() + "m"
  } else if rest == 0 {
    hours.to_string() + "h"
  } else {
    hours.to_string() + "h" + rest.to_string() + "m"
  }
}