///|
/// Convert an ASCII decimal character to a digit.  Callers validate the
/// input first; the fallback keeps validation functions total.
pub fn ascii_digit_value(c : Char) -> Int {
  if c >= '0' && c <= '9' {
    c.to_int() - '0'.to_int()
  } else {
    -1
  }
}

///|
pub fn valid_month(month : Int) -> Bool {
  month >= 1 && month <= 12
}

///|
pub fn days_in_month(year : Int, month : Int) -> Int {
  match month {
    2 => if is_leap_year(year) { 29 } else { 28 }
    4 | 6 | 9 | 11 => 30
    1 | 3 | 5 | 7 | 8 | 10 | 12 => 31
    _ => 0
  }
}

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

///|
pub fn decimal_value(value : String) -> Int {
  let mut result = 0
  for c in value {
    let digit = ascii_digit_value(c)
    if digit >= 0 {
      result = result * 10 + digit
    }
  }
  result
}

///|
pub fn valid_calendar_date(year : Int, month : Int, day : Int) -> Bool {
  year >= 1 &&
  valid_month(month) &&
  day >= 1 &&
  day <= days_in_month(year, month)
}

///|
pub fn parse_compact_date(value : String) -> (Int, Int, Int)? {
  let digits = keep_digits(value)
  if digits.length() != 8 {
    None
  } else {
    let year = decimal_value(digits[0:4].to_owned())
    let month = decimal_value(digits[4:6].to_owned())
    let day = decimal_value(digits[6:8].to_owned())
    if valid_calendar_date(year, month, day) {
      Some((year, month, day))
    } else {
      None
    }
  }
}

///|
pub fn valid_phone_number(value : String) -> Bool {
  let digits = keep_digits(value)
  let count = digits.length()
  if count == 11 {
    digits.has_prefix("1") &&
    ascii_digit_value(char_at_or(digits, 1, '0')) >= 3 &&
    ascii_digit_value(char_at_or(digits, 1, '0')) <= 9
  } else if count == 10 {
    value.to_array().any(fn(c) { c == '-' || c == ' ' || c == '.' || c == '(' })
  } else {
    count >= 7 && count <= 15
  }
}

///|
pub fn valid_email_address(value : String) -> Bool {
  let trimmed = trim_ascii_space(value)
  let mut at = -1
  for i in 0..= 0 {
    let position = at
    let local_part = trimmed[:position]
    let domain = trimmed[position + 1:]
    !local_part.is_empty() &&
    !domain.is_empty() &&
    domain.contains(".") &&
    !domain.contains("@") &&
    !domain.has_prefix(".") &&
    !domain.has_suffix(".") &&
    !local_part.has_prefix(".") &&
    !local_part.has_suffix(".")
  } else {
    false
  }
}

///|
pub fn valid_luhn(value : String) -> Bool {
  let digits = keep_digits(value)
  let allowed = value
    .to_array()
    .all(fn(c) { c.is_ascii_digit() || c == ' ' || c == '-' })
  if digits.is_empty() || !allowed {
    false
  } else {
    let mut sum = 0
    let mut double = false
    let chars = digits.to_array()
    let mut i = chars.length()
    while i > 0 {
      i -= 1
      let digit = ascii_digit_value(chars[i])
      let mut add = digit
      if double {
        add = digit * 2
        if add > 9 {
          add -= 9
        }
      }
      sum += add
      double = !double
    }
    sum % 10 == 0
  }
}

///|
pub fn valid_bank_card(value : String) -> Bool {
  let digits = keep_digits(value)
  digits.length() >= 13 && digits.length() <= 19 && valid_luhn(value)
}

///|
pub fn valid_ipv4(value : String) -> Bool {
  let parts = value.split(".").to_array()
  if parts.length() != 4 {
    false
  } else {
    parts.all(fn(part) {
      let item = part.to_owned()
      if item.length() >= 1 && item.length() <= 3 && all_ascii_digits(item) {
        let parsed = decimal_value(item)
        parsed >= 0 && parsed <= 255
      } else {
        false
      }
    })
  }
}

///|
pub fn valid_hex_color(value : String) -> Bool {
  let text = if value.has_prefix("#") { value[1:] } else { value }
  (text.length() == 3 || text.length() == 6 || text.length() == 8) &&
  all_ascii_hex(text.to_owned())
}

///|
pub fn valid_chinese_id(value : String) -> Bool {
  let id = trim_ascii_space(value)
  if id.length() != 18 {
    false
  } else {
    let chars = id.to_array()
    let first_seventeen = id[:17]
    if !all_ascii_digits(first_seventeen.to_owned()) {
      false
    } else {
      let year = decimal_value(id[6:10].to_owned())
      let month = decimal_value(id[10:12].to_owned())
      let day = decimal_value(id[12:14].to_owned())
      let final_char = chars[17]
      if !valid_calendar_date(year, month, day) ||
        !(final_char.is_ascii_digit() || final_char == 'x' || final_char == 'X') {
        false
      } else {
        let weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
        let checks = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
        let mut total = 0
        for i in 0..<17 {
          total += ascii_digit_value(chars[i]) * weights[i]
        }
        let expected = checks[total % 11]
        final_char.to_string().to_upper() == expected.to_string()
      }
    }
  }
}

///|
pub fn valid_passport_number(value : String) -> Bool {
  let text = trim_ascii_space(value).to_upper()
  let chars = text.to_array()
  chars.length() >= 6 &&
  chars.length() <= 12 &&
  chars[0].is_ascii_alphabetic() &&
  chars[1:].all(is_ascii_alphanumeric_char)
}

///|
pub fn valid_postal_code(value : String, locale : LocaleHint) -> Bool {
  let text = trim_ascii_space(value)
  match locale {
    Chinese => text.length() == 6 && all_ascii_digits(text)
    English =>
      text.length() >= 5 &&
      text.length() <= 10 &&
      text
      .to_array()
      .all(fn(c) { is_ascii_alphanumeric_char(c) || c == ' ' || c == '-' })
    Mixed | Neutral =>
      text.length() >= 4 &&
      text.length() <= 10 &&
      text
      .to_array()
      .all(fn(c) { is_ascii_alphanumeric_char(c) || c == ' ' || c == '-' })
  }
}

///|
pub fn valid_imei(value : String) -> Bool {
  let digits = keep_digits(value)
  digits.length() == 15 && valid_luhn(digits)
}

///|
pub fn valid_mac_address(value : String) -> Bool {
  let normalized = value
    .replace_all(old=":", new="")
    .replace_all(old="-", new="")
  normalized.length() == 12 && all_ascii_hex(normalized)
}

///|
pub fn valid_uuid(value : String) -> Bool {
  let parts = value.split("-").to_array()
  parts.length() == 5 &&
  parts[0].length() == 8 &&
  parts[1].length() == 4 &&
  parts[2].length() == 4 &&
  parts[3].length() == 4 &&
  parts[4].length() == 12 &&
  parts.all(fn(part) { all_ascii_hex(part.to_owned()) })
}

///|
pub fn valid_accession(value : String) -> Bool {
  let text = trim_ascii_space(value).to_upper()
  let chars = text.to_array()
  chars.length() >= 5 &&
  chars.length() <= 20 &&
  chars[0].is_ascii_alphabetic() &&
  chars.any(fn(c) { c.is_ascii_digit() }) &&
  chars.all(fn(c) { is_ascii_alphanumeric_char(c) || c == '_' || c == '.' })
}

///|
pub fn valid_lab_code(value : String) -> Bool {
  let text = trim_ascii_space(value).to_upper()
  let chars = text.to_array()
  chars.length() >= 4 &&
  chars.length() <= 24 &&
  chars[0].is_ascii_alphabetic() &&
  chars.any(fn(c) { c.is_ascii_digit() }) &&
  chars.all(fn(c) { is_ascii_alphanumeric_char(c) || c == '-' || c == '_' })
}

///|
pub fn valid_iban_prefix(value : String) -> Bool {
  let text = trim_ascii_space(value).replace_all(old=" ", new="").to_upper()
  text.length() >= 15 &&
  text.length() <= 34 &&
  char_at_or(text, 0, ' ').is_ascii_alphabetic() &&
  char_at_or(text, 1, ' ').is_ascii_alphabetic() &&
  char_at_or(text, 2, ' ').is_ascii_digit() &&
  char_at_or(text, 3, ' ').is_ascii_digit() &&
  text[4:].to_owned().to_array().all(is_ascii_alphanumeric_char)
}

///|
pub fn checksum_mod11(values : Array[Int], weights : Array[Int]) -> Int? {
  if values.length() != weights.length() || values.is_empty() {
    None
  } else {
    let mut sum = 0
    for i in 0.. String {
  keep_ascii_letters_and_digits(value).to_upper()
}

///|
pub fn identifier_entropy_hint(value : String) -> Int {
  let classes = [
    value.to_array().any(fn(c) { c.is_ascii_digit() }),
    value.to_array().any(fn(c) { c.is_ascii_lowercase() }),
    value.to_array().any(fn(c) { c.is_ascii_uppercase() }),
    value.to_array().any(is_cjk_char),
    value.to_array().any(is_ascii_punctuation_char),
  ]
  classes.filter(fn(item) { item }).length()
}