///|
pub(all) struct Date {
  year : Int
  month : Int
  day : Int
} derive(Eq, Debug)

///|
pub extend Date with Eq::{equal, not_equal}

///|
pub extend Date with @moonbitlang/core/debug.Debug::{to_repr}

///|
fn leap_year(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(year) { 29 } else { 28 }
    4 | 6 | 9 | 11 => 30
    _ => 31
  }
}

///|
pub fn Date::new(year : Int, month : Int, day : Int) -> Result[Date, Issue] {
  if year < 1 || year > 9999 || month < 1 || month > 12 {
    return Err(
      issue("invalid_date", "date", "Year must be 1..9999 and month 1..12"),
    )
  }
  if day < 1 || day > month_days(year, month) {
    return Err(
      issue("invalid_date", "date", "Day does not exist in this month"),
    )
  }
  Ok({ year, month, day, })
}

///|
fn decimal_digits(text : String) -> Int? {
  if text.is_empty() {
    return None
  }
  let mut number = 0
  for c in text.iter() {
    if c < '0' || c > '9' {
      return None
    }
    number = number * 10 + c.to_int() - 48
  }
  Some(number)
}

///|
pub fn parse_date(text : String) -> Result[Date, Issue] {
  if text.length() != 10 || text[4] != 45 || text[7] != 45 {
    return Err(issue("invalid_date", "date", "Expected YYYY-MM-DD"))
  }
  match
    (
      decimal_digits(text[0:4].to_owned()),
      decimal_digits(text[5:7].to_owned()),
      decimal_digits(text[8:10].to_owned()),
    ) {
    (Some(year), Some(month), Some(day)) => Date::new(year, month, day)
    _ => Err(issue("invalid_date", "date", "Expected decimal date components"))
  }
}

///|
fn pad_decimal(number : Int, width : Int) -> String {
  let text = number.to_string()
  let out = StringBuilder()
  for _ in 0..<(width - text.length()).max(0) {
    out.write_char('0')
  }
  out.write_string(text)
  out.to_string()
}

///|
pub fn Date::to_string(self : Date) -> String {
  pad_decimal(self.year, 4) +
  "-" +
  pad_decimal(self.month, 2) +
  "-" +
  pad_decimal(self.day, 2)
}

///|
fn days_before_year(year : Int) -> Int {
  let previous = year - 1
  previous * 365 + previous / 4 - previous / 100 + previous / 400
}

///|
pub fn Date::ordinal(self : Date) -> Result[Int, Issue] {
  match Date::new(self.year, self.month, self.day) {
    Err(error) => Err(error)
    Ok(_) => {
      let mut days = days_before_year(self.year) + self.day - 1
      for month in 1.. Result[Date, Issue] {
  let ordinal = match self.ordinal() {
    Ok(n) => n.to_int64() + delta.to_int64()
    Err(error) => return Err(error)
  }
  if ordinal < 0L || ordinal >= days_before_year(10000).to_int64() {
    return Err(
      issue("date_overflow", "date", "Date offset exceeds years 1..9999"),
    )
  }
  let target = ordinal.to_int()
  let mut low = 1
  let mut high = 10000
  while low + 1 < high {
    let middle = low + (high - low) / 2
    if days_before_year(middle) <= target {
      low = middle
    } else {
      high = middle
    }
  }
  let mut rest = target - days_before_year(low)
  let mut month = 1
  while rest >= month_days(low, month) {
    rest -= month_days(low, month)
    month += 1
  }
  Ok({ year: low, month, day: rest + 1, })
}

///|
/// UTC reference timestamps have second precision, no leap seconds or offsets.
fn reference_date(text : String) -> Result[Date, Issue] {
  if text.length() != 20 ||
    text[10] != 84 ||
    text[13] != 58 ||
    text[16] != 58 ||
    text[19] != 90 {
    return Err(
      issue(
        "invalid_reference_time", "reference_time", "Expected YYYY-MM-DDTHH:MM:SSZ",
      ),
    )
  }
  match
    (
      decimal_digits(text[11:13].to_owned()),
      decimal_digits(text[14:16].to_owned()),
      decimal_digits(text[17:19].to_owned()),
    ) {
    (Some(hour), Some(minute), Some(second)) =>
      if hour > 23 || minute > 59 || second > 59 {
        return Err(
          issue(
            "invalid_reference_time", "reference_time", "Invalid UTC clock components",
          ),
        )
      }
    _ =>
      return Err(
        issue(
          "invalid_reference_time", "reference_time", "Expected numeric UTC clock components",
        ),
      )
  }
  parse_date(text[0:10].to_owned())
}