///|
fn decimal_digits(chars : Array[Char], start : Int32, length : Int32) -> Int32? {
  if start < 0 || length <= 0 || start + length > chars.length() {
    return None
  }
  let mut result = 0
  for index in start..<(start + length) {
    let digit = match chars[index] {
      '0' => 0
      '1' => 1
      '2' => 2
      '3' => 3
      '4' => 4
      '5' => 5
      '6' => 6
      '7' => 7
      '8' => 8
      '9' => 9
      _ => return None
    }
    result = result * 10 + digit
  }
  Some(result)
}

///|
fn timezone_start(chars : Array[Char]) -> Int32? {
  let mut index = 10
  while index < chars.length() {
    match chars[index] {
      'Z' | 'z' | '+' | '-' => return Some(index)
      _ => index += 1
    }
  }
  None
}

///|
fn parse_timezone_offset(
  chars : Array[Char],
  start : Int32,
) -> Int32 raise DateError {
  if start >= chars.length() {
    return 0
  }
  if (chars[start] == 'Z' || chars[start] == 'z') && start + 1 == chars.length() {
    return 0
  }
  let sign = match chars[start] {
    '+' => 1
    '-' => -1
    _ =>
      raise DateError::InvalidFormat(message="expected UTC offset or Z suffix")
  }
  let digits = chars.length() - start - 1
  let hour = decimal_digits(chars, start + 1, 2).unwrap_or(-1)
  let (minute, second) = match digits {
    2 => (0, 0)
    4 => (decimal_digits(chars, start + 3, 2).unwrap_or(-1), 0)
    5 if chars[start + 3] == ':' =>
      (decimal_digits(chars, start + 4, 2).unwrap_or(-1), 0)
    6 =>
      (
        decimal_digits(chars, start + 3, 2).unwrap_or(-1),
        decimal_digits(chars, start + 5, 2).unwrap_or(-1),
      )
    8 if chars[start + 3] == ':' && chars[start + 6] == ':' =>
      (
        decimal_digits(chars, start + 4, 2).unwrap_or(-1),
        decimal_digits(chars, start + 7, 2).unwrap_or(-1),
      )
    _ =>
      raise DateError::InvalidFormat(
        message="UTC offset must be +HH, +HHMM[SS], or +HH:MM[:SS]",
      )
  }
  if hour < 0 ||
    hour > 23 ||
    minute < 0 ||
    minute > 59 ||
    second < 0 ||
    second > 59 {
    raise DateError::InvalidFormat(message="UTC offset is outside its range")
  }
  sign * (hour * 3600 + minute * 60 + second)
}

///|
/// Parse `YYYY-MM-DD` with an optional `T`/space-separated
/// `HH:MM[:SS[.ffffff]]` and fixed UTC offset (`Z`, `+HH`, `+HHMM[SS]`, or
/// `+HH:MM[:SS]`). Fractional seconds contain one to six digits and are padded on
/// the right.
pub fn parse_iso(value : String) -> DateTime raise DateError {
  let chars = value.to_array()
  if chars.length() < 10 {
    raise DateError::InvalidFormat(message="expected YYYY-MM-DD")
  }
  if chars[4] != '-' || chars[7] != '-' {
    raise DateError::InvalidFormat(message="expected YYYY-MM-DD")
  }
  let year = match decimal_digits(chars, 0, 4) {
    Some(value) => value
    None => raise DateError::InvalidFormat(message="invalid year")
  }
  let month = match decimal_digits(chars, 5, 2) {
    Some(value) => value
    None => raise DateError::InvalidFormat(message="invalid month")
  }
  let day = match decimal_digits(chars, 8, 2) {
    Some(value) => value
    None => raise DateError::InvalidFormat(message="invalid day")
  }
  let timezone_index = timezone_start(chars)
  let base_end = match timezone_index {
    Some(index) => index
    None => chars.length()
  }
  let offset_seconds = match timezone_index {
    Some(index) => parse_timezone_offset(chars, index)
    None => 0
  }
  if base_end == 10 && chars.length() == 10 {
    return datetime(year, month, day)
  }
  if base_end == 10 {
    raise DateError::InvalidFormat(message="expected time before UTC offset")
  }
  if chars[10] != 'T' && chars[10] != ' ' {
    raise DateError::InvalidFormat(message="expected T or space before time")
  }
  if base_end < 16 || chars[13] != ':' {
    raise DateError::InvalidFormat(message="expected HH:MM")
  }
  let hour = match decimal_digits(chars, 11, 2) {
    Some(value) => value
    None => raise DateError::InvalidFormat(message="invalid hour")
  }
  let minute = match decimal_digits(chars, 14, 2) {
    Some(value) => value
    None => raise DateError::InvalidFormat(message="invalid minute")
  }
  let second = if base_end == 16 {
    0
  } else {
    if base_end < 19 || chars[16] != ':' {
      raise DateError::InvalidFormat(message="expected optional :SS")
    }
    match decimal_digits(chars, 17, 2) {
      Some(value) => value
      None => raise DateError::InvalidFormat(message="invalid second")
    }
  }
  let microsecond = if base_end == 16 || base_end == 19 {
    0
  } else {
    if base_end < 21 || chars[19] != '.' {
      raise DateError::InvalidFormat(
        message="expected optional fractional seconds",
      )
    }
    let length = base_end - 20
    if length < 1 || length > 6 {
      raise DateError::InvalidFormat(
        message="fractional seconds must contain 1..6 digits",
      )
    }
    let value = match decimal_digits(chars, 20, length) {
      Some(value) => value
      None =>
        raise DateError::InvalidFormat(message="invalid fractional seconds")
    }
    let mut scaled = value
    for _ in length..<6 {
      scaled *= 10
    }
    scaled
  }
  datetime(
    year,
    month,
    day,
    hour~,
    minute~,
    second~,
    microsecond~,
    offset_seconds~,
  )
}