// Python `helper.is_iso_date` / `is_iso_datetime` / `is_date_unit`.

///|
let date_units : Array[String] = [
  "day", "week", "month", "quarter", "year", "year_month",
]

///|
fn is_date_unit(expression : @core.Expr?) -> Bool {
  match expression {
    Some(e) => date_units.contains(@core.py_lower(e.name()))
    None => false
  }
}

///|
fn digits_at(chars : Array[Char], start : Int, n : Int) -> Int? {
  if start + n > chars.length() {
    return None
  }
  let mut v = 0
  for i in start..<(start + n) {
    let c = chars[i]
    if c < '0' || c > '9' {
      return None
    }
    v = v * 10 + (c.to_int() - '0'.to_int())
  }
  Some(v)
}

///|
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 (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 {
        29
      } else {
        28
      }
    _ => 0
  }
}

///|
/// Parses an ISO date prefix, returning the number of chars consumed (Python 3.11+
/// `date.fromisoformat` formats: YYYY-MM-DD, YYYYMMDD, YYYY-Www[-D], YYYYWww[D]).
fn parse_iso_date_prefix(chars : Array[Char]) -> Int? {
  let year = match digits_at(chars, 0, 4) {
    Some(y) if y >= 1 => y
    _ => return None
  }
  let n = chars.length()
  if n >= 5 && chars[4] == '-' {
    if n >= 6 && chars[5] == 'W' {
      match digits_at(chars, 6, 2) {
        Some(w) if w >= 1 && w <= 53 =>
          if n >= 10 && chars[8] == '-' {
            match digits_at(chars, 9, 1) {
              Some(d) if d >= 1 && d <= 7 => Some(10)
              _ => None
            }
          } else {
            Some(8)
          }
        _ => None
      }
    } else {
      match (digits_at(chars, 5, 2), n >= 8 && chars[7] == '-', digits_at(chars, 8, 2)) {
        (Some(m), true, Some(d)) if m >= 1 &&
          m <= 12 &&
          d >= 1 &&
          d <= days_in_month(year, m) => Some(10)
        _ => None
      }
    }
  } else if n >= 5 && chars[4] == 'W' {
    match digits_at(chars, 5, 2) {
      Some(w) if w >= 1 && w <= 53 =>
        match digits_at(chars, 7, 1) {
          Some(d) if d >= 1 && d <= 7 => Some(8)
          _ => Some(7)
        }
      _ => None
    }
  } else {
    match (digits_at(chars, 4, 2), digits_at(chars, 6, 2)) {
      (Some(m), Some(d)) if m >= 1 &&
        m <= 12 &&
        d >= 1 &&
        d <= days_in_month(year, m) => Some(8)
      _ => None
    }
  }
}

///|
fn is_iso_date(text : String) -> Bool {
  let chars = text.to_array()
  match parse_iso_date_prefix(chars) {
    Some(n) => n == chars.length()
    None => false
  }
}

///|
/// Parses an ISO time (HH[:MM[:SS[.f+]]] or HH[MM[SS[.f+]]]) followed by an optional
/// UTC offset, returning whether the whole slice was consumed.
fn is_iso_time(chars : ArrayView[Char]) -> Bool {
  let n = chars.length()
  let arr = chars.to_array()
  let hh = match digits_at(arr, 0, 2) {
    Some(h) if h <= 23 => h
    _ => return false
  }
  ignore(hh)
  let mut i = 2
  let extended = i < n && arr[i] == ':'
  // minutes
  if i < n && arr[i] != '+' && arr[i] != '-' && arr[i] != 'Z' {
    if extended {
      i += 1
    }
    match digits_at(arr, i, 2) {
      Some(m) if m <= 59 => i += 2
      _ => return false
    }
    // seconds
    if i < n && arr[i] != '+' && arr[i] != '-' && arr[i] != 'Z' {
      if extended {
        if arr[i] != ':' {
          return false
        }
        i += 1
      }
      match digits_at(arr, i, 2) {
        Some(s) if s <= 59 => i += 2
        _ => return false
      }
      // fraction
      if i < n && (arr[i] == '.' || arr[i] == ',') {
        i += 1
        let start = i
        while i < n && arr[i] >= '0' && arr[i] <= '9' {
          i += 1
        }
        if i == start {
          return false
        }
      }
    }
  }
  if i == n {
    return true
  }
  // UTC offset
  if arr[i] == 'Z' {
    return i + 1 == n
  }
  if arr[i] == '+' || arr[i] == '-' {
    i += 1
    match digits_at(arr, i, 2) {
      Some(h) if h <= 23 => i += 2
      _ => return false
    }
    if i == n {
      return true
    }
    if arr[i] == ':' {
      i += 1
    }
    match digits_at(arr, i, 2) {
      Some(m) if m <= 59 => i += 2
      _ => return false
    }
    if i == n {
      return true
    }
    if arr[i] == ':' {
      i += 1
    }
    match digits_at(arr, i, 2) {
      Some(s) if s <= 59 => i += 2
      _ => return false
    }
    if i < n && arr[i] == '.' {
      i += 1
      while i < n && arr[i] >= '0' && arr[i] <= '9' {
        i += 1
      }
    }
    return i == n
  }
  false
}

///|
fn is_iso_datetime(text : String) -> Bool {
  let chars = text.to_array()
  let n = match parse_iso_date_prefix(chars) {
    Some(n) => n
    None => return false
  }
  if n == chars.length() {
    return true
  }
  // a single separator char followed by a time
  if n + 1 >= chars.length() {
    return false
  }
  is_iso_time(chars[n + 1:])
}