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

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

///|
fn decimal_part(
  s : String,
  start : Int32,
  end : Int32,
) -> Int32 raise TemplateError {
  if start >= end || end > s.length() {
    raise Invalid("invalid date component")
  }
  let mut n = 0
  for i = start; i < end; i = i + 1 {
    let c = s.code_unit_at(i).to_int()
    if !digit(c) {
      raise Invalid("expected decimal date component")
    }
    n = n * 10 + c - 48
  }
  n
}

///|
fn offset_seconds(offset : String, b : Budget) -> Double raise TemplateError {
  let s = offset.trim().to_owned()
  let mut i = 0
  let mut sign = 1.0
  if s.has_prefix("-") || s.has_prefix("+") {
    if s.has_prefix("-") {
      sign = -1
    }
    i = 1
  }
  let mut last = -1
  let mut total = 0.0
  while i < s.length() {
    b.tick(0)
    while i < s.length() && [32, 9, 10, 13].contains(s.code_unit_at(i).to_int()) {
      i += 1
    }
    if i == s.length() {
      break
    }
    let begin = i
    while i < s.length() && digit(s.code_unit_at(i).to_int()) {
      i += 1
    }
    if begin == i {
      raise Invalid("invalid relative time")
    }
    let count = @string.parse_double(s[begin:i]) catch {
      _ => raise Invalid("relative time magnitude")
    }
    while i < s.length() && [32, 9, 10, 13].contains(s.code_unit_at(i).to_int()) {
      i += 1
    }
    let begin = i
    while i < s.length() && alpha(s.code_unit_at(i).to_int()) {
      i += 1
    }
    let unit = s[begin:i].to_owned()
    let (order, factor) = match unit {
      "years" | "year" | "yr" | "y" => (0, 31536000.0)
      "months" | "month" | "mo" => (1, 2592000.0)
      "weeks" | "week" | "wk" | "w" => (2, 604800.0)
      "days" | "day" | "d" => (3, 86400.0)
      "hours" | "hour" | "hr" | "h" => (4, 3600.0)
      "minutes" | "minute" | "min" | "m" => (5, 60.0)
      "seconds" | "second" | "sec" | "s" => (6, 1.0)
      _ => raise Invalid("invalid relative time unit")
    }
    if order <= last {
      raise Invalid("relative units must be unique and descending")
    }
    last = order
    total += count * factor
    if total > 315537897599.0 {
      raise Limit("relative time exceeds year range")
    }
  }
  total * sign
}

///|
fn pad(n : Int32, width : Int32) -> String {
  let mut s = n.to_string()
  while s.length() < width {
    s = "0" + s
  }
  s
}

///|
fn relative_time(
  offset : String,
  reference : String,
  b : Budget,
) -> String raise TemplateError {
  if reference.length() != 24 ||
    reference[4:5] != "-" ||
    reference[7:8] != "-" ||
    reference[10:11] != "T" ||
    reference[13:14] != ":" ||
    reference[16:17] != ":" ||
    reference[19:20] != "." ||
    reference[23:24] != "Z" {
    raise Invalid("reference must be YYYY-MM-DDTHH:mm:ss.sssZ")
  }
  let mut year = decimal_part(reference, 0, 4)
  let mut month = decimal_part(reference, 5, 7)
  let mut day = decimal_part(reference, 8, 10)
  let hour = decimal_part(reference, 11, 13)
  let minute = decimal_part(reference, 14, 16)
  let second = decimal_part(reference, 17, 19)
  let ms = decimal_part(reference, 20, 23)
  if year < 1 ||
    month < 1 ||
    month > 12 ||
    day < 1 ||
    day > month_days(year, month) ||
    hour > 23 ||
    minute > 59 ||
    second > 59 {
    raise Invalid("invalid UTC date")
  }
  let seconds = offset_seconds(offset, b) +
    (hour * 3600 + minute * 60 + second).to_double()
  let mut days = @math.floor(seconds / 86400).to_int()
  let rest = (seconds - days.to_double() * 86400).to_int()
  while days > 0 {
    b.tick(0)
    let count = month_days(year, month) - day + 1
    if days < count {
      day += days
      days = 0
    } else {
      days -= count
      day = 1
      month += 1
      if month > 12 {
        month = 1
        year += 1
      }
    }
    if year > 9999 {
      raise Limit("date exceeds year 9999")
    }
  }
  while days < 0 {
    b.tick(0)
    if -days < day {
      day += days
      days = 0
    } else {
      days += day
      month -= 1
      if month < 1 {
        month = 12
        year -= 1
      }
      if year < 1 {
        raise Limit("date precedes year 1")
      }
      day = month_days(year, month)
    }
  }
  pad(year, 4) +
  "-" +
  pad(month, 2) +
  "-" +
  pad(day, 2) +
  "T" +
  pad(rest / 3600, 2) +
  ":" +
  pad(rest / 60 % 60, 2) +
  ":" +
  pad(rest % 60, 2) +
  "." +
  pad(ms, 3) +
  "Z"
}