///|
fn decimal_digit(char : Char) -> Int? {
  if char.is_ascii_digit() {
    Some(char.to_int() - '0'.to_int())
  } else {
    None
  }
}

///|
fn parse_fixed_decimal(chars : Array[Char], start : Int, length : Int) -> Int? {
  if start < 0 || length <= 0 || start + length > chars.length() {
    return None
  }
  let mut value = 0
  for index = start; index < start + length; index = index + 1 {
    guard decimal_digit(chars[index]) is Some(digit) else { return None }
    value = value * 10 + digit
  }
  Some(value)
}

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

///|
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 is_leap_year(year) { 29 } else { 28 }
    _ => 0
  }
}

///|
fn is_iso_date_chars(chars : Array[Char], start : Int) -> Bool {
  if start < 0 || start + 10 > chars.length() {
    return false
  }
  if chars[start + 4] != '-' || chars[start + 7] != '-' {
    return false
  }
  guard parse_fixed_decimal(chars, start, 4) is Some(year) else { return false }
  guard parse_fixed_decimal(chars, start + 5, 2) is Some(month) else {
    return false
  }
  guard parse_fixed_decimal(chars, start + 8, 2) is Some(day) else {
    return false
  }
  year >= 1 &&
  month >= 1 &&
  month <= 12 &&
  day >= 1 &&
  day <= days_in_month(year, month)
}

///|
/// Validate an ISO 8601 calendar date in `YYYY-MM-DD` form.
fn is_iso_date_string(text : String) -> Bool {
  let chars = text.to_array()
  chars.length() == 10 && is_iso_date_chars(chars, 0)
}

///|
fn parse_timezone(chars : Array[Char], start : Int) -> Bool {
  if start >= chars.length() {
    return false
  }
  if chars[start] == 'Z' || chars[start] == 'z' {
    return start + 1 == chars.length()
  }
  if chars[start] != '+' && chars[start] != '-' {
    return false
  }
  if start + 6 != chars.length() || chars[start + 3] != ':' {
    return false
  }
  guard parse_fixed_decimal(chars, start + 1, 2) is Some(hours) else {
    return false
  }
  guard parse_fixed_decimal(chars, start + 4, 2) is Some(minutes) else {
    return false
  }
  hours <= 23 && minutes <= 59
}

///|
/// Validate a practical RFC 3339 timestamp with seconds and a timezone.
fn is_rfc3339_datetime_string(text : String) -> Bool {
  let chars = text.to_array()
  if chars.length() < 20 || !is_iso_date_chars(chars, 0) {
    return false
  }
  if (chars[10] != 'T' && chars[10] != 't') ||
    chars[13] != ':' ||
    chars[16] != ':' {
    return false
  }
  guard parse_fixed_decimal(chars, 11, 2) is Some(hours) else { return false }
  guard parse_fixed_decimal(chars, 14, 2) is Some(minutes) else { return false }
  guard parse_fixed_decimal(chars, 17, 2) is Some(seconds) else { return false }
  if hours > 23 || minutes > 59 || seconds > 59 {
    return false
  }
  let mut timezone_start = 19
  if chars.get(timezone_start) == Some('.') {
    timezone_start = timezone_start + 1
    let fraction_start = timezone_start
    while chars
          .get(timezone_start)
          .map(fn(ch) { ch.is_ascii_digit() })
          .unwrap_or(false) {
      timezone_start = timezone_start + 1
    }
    if timezone_start == fraction_start {
      return false
    }
  }
  parse_timezone(chars, timezone_start)
}

///|
fn is_ipv4_string(text : String) -> Bool {
  let chars = text.to_array()
  if chars.is_empty() {
    return false
  }
  let mut cursor = 0
  let mut segments = 0
  while cursor < chars.length() {
    if segments == 4 {
      return false
    }
    let start = cursor
    let mut value = 0
    while cursor < chars.length() && chars[cursor].is_ascii_digit() {
      value = value * 10 + chars[cursor].to_int() - '0'.to_int()
      cursor = cursor + 1
    }
    let digits = cursor - start
    if digits == 0 || digits > 3 || value > 255 {
      return false
    }
    if digits > 1 && chars[start] == '0' {
      return false
    }
    segments = segments + 1
    if cursor == chars.length() {
      break
    }
    if chars[cursor] != '.' {
      return false
    }
    cursor = cursor + 1
    if cursor == chars.length() {
      return false
    }
  }
  segments == 4
}

///|
fn is_email_local_char(char : Char) -> Bool {
  char.is_ascii_alphabetic() ||
  char.is_ascii_digit() ||
  char == '.' ||
  char == '!' ||
  char == '#' ||
  char == '$' ||
  char == '%' ||
  char == '&' ||
  char == '\'' ||
  char == '*' ||
  char == '+' ||
  char == '-' ||
  char == '/' ||
  char == '=' ||
  char == '?' ||
  char == '^' ||
  char == '_' ||
  char == '`' ||
  char == '{' ||
  char == '|' ||
  char == '}' ||
  char == '~'
}

///|
fn is_email_domain(chars : Array[Char], start : Int) -> Bool {
  if start >= chars.length() || chars.length() - start > 253 {
    return false
  }
  let mut cursor = start
  let mut labels = 0
  while cursor < chars.length() {
    let label_start = cursor
    while cursor < chars.length() && chars[cursor] != '.' {
      let char = chars[cursor]
      if !char.is_ascii_alphabetic() && !char.is_ascii_digit() && char != '-' {
        return false
      }
      cursor = cursor + 1
    }
    let label_length = cursor - label_start
    if label_length == 0 ||
      label_length > 63 ||
      chars[label_start] == '-' ||
      chars[cursor - 1] == '-' {
      return false
    }
    labels = labels + 1
    if cursor < chars.length() {
      cursor = cursor + 1
      if cursor == chars.length() {
        return false
      }
    }
  }
  labels >= 2
}

///|
/// Validate a conservative ASCII email-address syntax.
fn is_email_string(text : String) -> Bool {
  let chars = text.to_array()
  if chars.length() < 3 || chars.length() > 254 {
    return false
  }
  let mut at = -1
  for index, char in chars {
    if char == '@' {
      if at >= 0 {
        return false
      }
      at = index
    }
  }
  if at <= 0 || at > 64 || at + 1 >= chars.length() {
    return false
  }
  if chars[0] == '.' || chars[at - 1] == '.' {
    return false
  }
  for index = 0; index < at; index = index + 1 {
    if !is_email_local_char(chars[index]) ||
      (chars[index] == '.' && index + 1 < at && chars[index + 1] == '.') {
      return false
    }
  }
  is_email_domain(chars, at + 1)
}

///|
fn is_uuid_string(text : String) -> Bool {
  let chars = text.to_array()
  if chars.length() != 36 {
    return false
  }
  for index, char in chars {
    if index == 8 || index == 13 || index == 18 || index == 23 {
      if char != '-' {
        return false
      }
    } else if !char.is_ascii_hexdigit() {
      return false
    }
  }
  true
}

///|
fn is_semver_identifier_char(char : Char) -> Bool {
  char.is_ascii_alphabetic() || char.is_ascii_digit() || char == '-'
}

///|
fn scan_semver_identifiers(
  chars : Array[Char],
  start : Int,
  end : Int,
  reject_numeric_leading_zero : Bool,
) -> Bool {
  if start >= end {
    return false
  }
  let mut cursor = start
  while cursor < end {
    let identifier_start = cursor
    let mut numeric = true
    while cursor < end && chars[cursor] != '.' {
      if !is_semver_identifier_char(chars[cursor]) {
        return false
      }
      if !chars[cursor].is_ascii_digit() {
        numeric = false
      }
      cursor = cursor + 1
    }
    if cursor == identifier_start ||
      (
        reject_numeric_leading_zero &&
        numeric &&
        cursor - identifier_start > 1 &&
        chars[identifier_start] == '0'
      ) {
      return false
    }
    if cursor < end {
      cursor = cursor + 1
      if cursor == end {
        return false
      }
    }
  }
  true
}

///|
fn scan_semver_core(chars : Array[Char], end : Int) -> Bool {
  let mut cursor = 0
  let mut components = 0
  while cursor < end {
    let start = cursor
    while cursor < end && chars[cursor].is_ascii_digit() {
      cursor = cursor + 1
    }
    if cursor == start || (cursor - start > 1 && chars[start] == '0') {
      return false
    }
    components = components + 1
    if cursor == end {
      break
    }
    if chars[cursor] != '.' || components >= 3 {
      return false
    }
    cursor = cursor + 1
  }
  components == 3
}

///|
fn is_semver_string(text : String) -> Bool {
  let chars = text.to_array()
  if chars.is_empty() {
    return false
  }
  let mut core_end = chars.length()
  let mut prerelease_start = -1
  let mut build_start = -1
  for index, char in chars {
    if char == '-' && prerelease_start < 0 && build_start < 0 {
      prerelease_start = index + 1
      core_end = index
    } else if char == '+' {
      if build_start >= 0 {
        return false
      }
      build_start = index + 1
      if prerelease_start < 0 {
        core_end = index
      }
    }
  }
  if !scan_semver_core(chars, core_end) {
    return false
  }
  if prerelease_start >= 0 {
    let prerelease_end = if build_start >= 0 {
      build_start - 1
    } else {
      chars.length()
    }
    if !scan_semver_identifiers(chars, prerelease_start, prerelease_end, true) {
      return false
    }
  }
  if build_start >= 0 &&
    !scan_semver_identifiers(chars, build_start, chars.length(), false) {
    return false
  }
  true
}

///|
fn validate_literal_for_function(name : String, text : String) -> Bool? {
  match name {
    "is_date" => Some(is_iso_date_string(text))
    "is_datetime" => Some(is_rfc3339_datetime_string(text))
    "is_ipv4" => Some(is_ipv4_string(text))
    "is_email" => Some(is_email_string(text))
    "is_uuid" => Some(is_uuid_string(text))
    "is_semver" => Some(is_semver_string(text))
    _ => None
  }
}