///|
/// Gregorian UTC components. Not a timezone or leap-second model.
pub(all) struct UtcDateTime {
  year : Int
  month : Int
  day : Int
  hour : Int
  minute : Int
  second : Int
  millisecond : Int
} derive(Eq, Debug)

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

///|
fn month_days(year : Int, month : Int) -> Int {
  match month {
    2 => if leap(year) { 29 } else { 28 }
    4 | 6 | 9 | 11 => 30
    _ => 31
  }
}

///|
/// Construct a UTC instant. Rejects nonexistent dates, leap seconds, and rollover.
pub fn utc(
  year : Int,
  month : Int,
  day : Int,
  hour? : Int = 0,
  minute? : Int = 0,
  second? : Int = 0,
  millisecond? : Int = 0,
) -> Result[Instant, InputError] {
  if year < 1900 ||
    year > 2150 ||
    month < 1 ||
    month > 12 ||
    day < 1 ||
    day > month_days(year, month) ||
    hour < 0 ||
    hour > 23 ||
    minute < 0 ||
    minute > 59 ||
    second < 0 ||
    second > 59 ||
    millisecond < 0 ||
    millisecond > 999 {
    return Err(InvalidDate)
  }
  let mut days = -25567
  for y = 1900; y < year; y = y + 1 {
    days += if leap(y) { 366 } else { 365 }
  }
  for m = 1; m < month; m = m + 1 {
    days += month_days(year, m)
  }
  days += day - 1
  instant(
    days.to_double() * DayMs +
    (hour * 3600000 + minute * 60000 + second * 1000 + millisecond).to_double(),
  )
}

///|
/// UTC breakdown. Event solvers can return neighbouring days outside the input domain.
pub fn Instant::to_utc(self : Instant) -> UtcDateTime {
  let midnight = @math.floor(self.ms / DayMs)
  let mut days = midnight.to_int() + 25567
  let mut year = 1900
  if days < 0 {
    year = 1899
    days += 365
  }
  while days >= (if leap(year) { 366 } else { 365 }) {
    days -= if leap(year) { 366 } else { 365 }
    year += 1
  }
  let mut month = 1
  while days >= month_days(year, month) {
    days -= month_days(year, month)
    month += 1
  }
  let ms = (self.ms - midnight * DayMs).to_int()
  {
    year,
    month,
    day: days + 1,
    hour: ms / 3600000,
    minute: ms / 60000 % 60,
    second: ms / 1000 % 60,
    millisecond: ms % 1000,
  }
}

///|
fn pad(n : Int, width : Int) -> String {
  let s = n.to_string()
  "0".repeat((width - s.length()).max(0)) + s
}

///|
/// Always YYYY-MM-DDTHH:MM:SS.sssZ; no machine-local timezone conversion.
pub fn Instant::to_iso_utc(self : Instant) -> String {
  let d = self.to_utc()
  pad(d.year, 4) +
  "-" +
  pad(d.month, 2) +
  "-" +
  pad(d.day, 2) +
  "T" +
  pad(d.hour, 2) +
  ":" +
  pad(d.minute, 2) +
  ":" +
  pad(d.second, 2) +
  "." +
  pad(d.millisecond, 3) +
  "Z"
}

///|
fn decimal_part(s : String, start : Int, len : Int) -> Int? {
  let mut n = 0
  for i = start; i < start + len; i = i + 1 {
    let c = s.at(i).to_int()
    if c < 48 || c > 57 {
      return None
    }
    n = n * 10 + c - 48
  }
  Some(n)
}

///|
/// Strict UTC forms YYYY-MM-DDTHH:MM:SSZ and YYYY-MM-DDTHH:MM:SS.sssZ only.
pub fn parse_utc(s : String) -> Result[Instant, InputError] {
  let n = s.length()
  if n != 20 && n != 24 {
    return Err(InvalidDate)
  }
  if s.at(4) != 45 ||
    s.at(7) != 45 ||
    s.at(10) != 84 ||
    s.at(13) != 58 ||
    s.at(16) != 58 ||
    s.at(n - 1) != 90 ||
    (n == 24 && s.at(19) != 46) {
    return Err(InvalidDate)
  }
  let parts = [(0, 4), (5, 2), (8, 2), (11, 2), (14, 2), (17, 2)]
  let fields = []
  for part in parts {
    match decimal_part(s, part.0, part.1) {
      Some(v) => fields.push(v)
      None => return Err(InvalidDate)
    }
  }
  let ms = if n == 24 {
    match decimal_part(s, 19 + 1, 3) {
      Some(v) => v
      None => return Err(InvalidDate)
    }
  } else {
    0
  }
  utc(
    fields[0],
    fields[1],
    fields[2],
    hour=fields[3],
    minute=fields[4],
    second=fields[5],
    millisecond=ms,
  )
}