///|
fn is_ascii_letter(c : Char) -> Bool {
  ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')
}

///|
fn is_digit_char(c : Char) -> Bool {
  '0' <= c && c <= '9'
}

///|
fn compact_code(input : String) -> String {
  normalize_code(input)
}

///|
fn digit_value(c : UInt16) -> Int {
  c.to_int() - ('0' : UInt16).to_int()
}

///|
fn parse_two_digits(s : String, at~ : Int) -> Int? {
  if at + 1 >= s.length() {
    None
  } else if is_digit_char(s.get_char(at).unwrap()) &&
    is_digit_char(s.get_char(at + 1).unwrap()) {
    Some(digit_value(s[at]) * 10 + digit_value(s[at + 1]))
  } else {
    None
  }
}

///|
pub fn parse_icd10(input : String) -> IcdCode raise IcdParseError {
  let cleaned = compact_code(input)
  if cleaned.is_empty() {
    raise IcdParseError::EmptyInput
  }
  if cleaned.length() < 3 {
    raise IcdParseError::InvalidCode(input)
  }
  guard cleaned.get_char(0) is Some(letter) && is_ascii_letter(letter) else {
    raise IcdParseError::InvalidCode(input)
  }
  guard parse_two_digits(cleaned, at=1) is Some(number) else {
    raise IcdParseError::InvalidCode(input)
  }
  let category = cleaned[:3].to_owned()
  let subcategory = if cleaned.length() > 3 {
    if cleaned[3] != ('.' : UInt16) || cleaned.length() > 8 {
      raise IcdParseError::InvalidCode(input)
    }
    let rest = cleaned[4:].to_owned()
    if rest.is_empty() || !rest.all(is_digit_char) {
      raise IcdParseError::InvalidCode(input)
    }
    Some(rest)
  } else {
    None
  }
  {
    edition: ICD10,
    raw: input,
    stem: category,
    category,
    subcategory,
    numeric: rank_parts(letter, number),
    extension: None,
  }
}

///|
pub fn parse_icd11(input : String) -> IcdCode raise IcdParseError {
  let cleaned = compact_code(input)
  if cleaned.is_empty() {
    raise IcdParseError::EmptyInput
  }
  let stem = match cleaned.split_once("&") {
    Some((left, right)) => {
      if right.is_empty() {
        raise IcdParseError::InvalidCode(input)
      }
      left.to_owned()
    }
    None => cleaned
  }
  if stem.length() != 4 ||
    !(stem.get_char(0) is Some(first) &&
    (is_digit_char(first) || is_ascii_letter(first))) ||
    !stem[1:].all(fn(c) { is_digit_char(c) || is_ascii_letter(c) }) {
    raise IcdParseError::InvalidCode(input)
  }
  {
    edition: ICD11,
    raw: input,
    stem,
    category: stem,
    subcategory: None,
    numeric: 0,
    extension: match cleaned.after("&") {
      Some(v) => Some(v.trim().to_owned().to_upper())
      None => None
    },
  }
}

///|
pub fn parse_code(
  input : String,
  edition? : IcdEdition = ICD10,
) -> IcdCode raise IcdParseError {
  match edition {
    ICD10 => parse_icd10(input)
    ICD11 => parse_icd11(input)
  }
}