///|
fn is_ws(c : Char) -> Bool {
  c == ' ' || c == '\t' || c == '\n' || c == '\r'
}

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

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

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

///|
fn digit_of(c : Char) -> Int? {
  if is_digit(c) {
    Some(c.to_int() - 48)
  } else {
    None
  }
}

///|
fn parse_i64(text : String) -> Int64? {
  let s = text.trim().to_owned()
  if s.is_empty() {
    return None
  }
  let chars = s.to_array()
  let mut i = 0
  let mut sign : Int64 = 1
  if chars[0] == '+' || chars[0] == '-' {
    if chars[0] == '-' {
      sign = -1
    }
    i = 1
  }
  if i >= chars.length() {
    return None
  }
  let mut n : Int64 = 0
  while i < chars.length() {
    match digit_of(chars[i]) {
      Some(d) => n = n * 10L + d.to_int64()
      None => return None
    }
    i = i + 1
  }
  Some(n * sign)
}

///|
fn parse_int(text : String) -> Int? {
  match parse_i64(text) {
    Some(n) =>
      if n > 2147483647L || n < -2147483648L {
        None
      } else {
        Some(n.to_int())
      }
    None => None
  }
}

///|
fn ascii_lower(text : String) -> String {
  text.to_lower()
}

///|
fn ascii_upper(text : String) -> String {
  text.to_upper()
}

///|
fn local_name(tag : String) -> String {
  match tag.split_once(":") {
    Some((_, rest)) => rest.to_owned()
    None => tag
  }
}

///|
fn html_escape(text : String) -> String {
  let out = StringBuilder()
  for c in text {
    match c {
      '&' => out.write_string("&")
      '<' => out.write_string("<")
      '>' => out.write_string(">")
      '"' => out.write_string(""")
      _ => out.write_char(c)
    }
  }
  out.to_string()
}

///|
fn decode_entity(body : String) -> String {
  if body == "amp" {
    "&"
  } else if body == "lt" {
    "<"
  } else if body == "gt" {
    ">"
  } else if body == "quot" {
    "\""
  } else if body == "apos" {
    "'"
  } else if body.has_prefix("#x") || body.has_prefix("#X") {
    match parse_hex(body[2:].to_owned()) {
      Some(n) => char_from_code(n)
      None => "&" + body + ";"
    }
  } else if body.has_prefix("#") {
    match parse_int(body[1:].to_owned()) {
      Some(n) => char_from_code(n)
      None => "&" + body + ";"
    }
  } else {
    "&" + body + ";"
  }
}

///|
fn parse_hex(text : String) -> Int? {
  if text.is_empty() {
    return None
  }
  let mut n = 0
  for c in text {
    let d = if c >= '0' && c <= '9' {
      c.to_int() - 48
    } else if c >= 'a' && c <= 'f' {
      c.to_int() - 87
    } else if c >= 'A' && c <= 'F' {
      c.to_int() - 55
    } else {
      return None
    }
    n = n * 16 + d
    if n > 1114111 {
      return None
    }
  }
  Some(n)
}

///|
fn char_from_code(code : Int) -> String {
  if code <= 0 {
    ""
  } else {
    let buf = StringBuilder()
    buf.write_char(Int::unsafe_to_char(code))
    buf.to_string()
  }
}

///|
fn split_ws(text : String) -> Array[String] {
  let out : Array[String] = []
  let buf = StringBuilder()
  for c in text {
    if is_ws(c) {
      let s = buf.to_string()
      if s != "" {
        out.push(s)
      }
      buf.reset()
    } else {
      buf.write_char(c)
    }
  }
  let last = buf.to_string()
  if last != "" {
    out.push(last)
  }
  out
}

///|
fn join_nonempty(parts : Array[String], sep : String) -> String {
  let kept : Array[String] = []
  for p in parts {
    if p != "" {
      kept.push(p)
    }
  }
  kept.join(sep)
}

///|
fn bool_word(text : String) -> Bool {
  let t = ascii_lower(text.trim().to_owned())
  t == "true" || t == "1" || t == "yes"
}

///|
fn roman_upper(n : Int) -> String {
  if n <= 0 {
    return ""
  }
  let vals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
  let syms = [
    "M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I",
  ]
  let mut x = n
  let out = StringBuilder()
  for i = 0; i < vals.length(); i = i + 1 {
    while x >= vals[i] {
      out.write_string(syms[i])
      x = x - vals[i]
    }
  }
  out.to_string()
}

///|
fn ordinal_en(n : Int) -> String {
  let abs = if n < 0 { -n } else { n }
  let mod100 = abs % 100
  let suffix = if mod100 >= 11 && mod100 <= 13 {
    "th"
  } else {
    match abs % 10 {
      1 => "st"
      2 => "nd"
      3 => "rd"
      _ => "th"
    }
  }
  n.to_string() + suffix
}

///|
fn is_csl_numeric(text : String) -> Bool {
  let s = text.trim().to_owned()
  if s.is_empty() {
    return false
  }
  let lower = ascii_lower(s)
  let mut i = 0
  let chars = lower.to_array()
  let mut saw_digit = false
  while i < chars.length() {
    let c = chars[i]
    if is_digit(c) {
      saw_digit = true
      i = i + 1
    } else if is_ws(c) ||
      c == ',' ||
      c == '.' ||
      c == '-' ||
      c == '/' ||
      c == '&' ||
      c == Int::unsafe_to_char(0x2013) {
      i = i + 1
    } else if c == 'a' &&
      i + 2 < chars.length() &&
      chars[i + 1] == 'n' &&
      chars[i + 2] == 'd' {
      i = i + 3
    } else {
      return false
    }
  }
  saw_digit
}

///|
fn first_int(text : String) -> Int? {
  let buf = StringBuilder()
  let mut seen = false
  for c in text {
    if is_digit(c) {
      buf.write_char(c)
      seen = true
    } else if seen {
      break
    }
  }
  if seen {
    parse_int(buf.to_string())
  } else {
    None
  }
}

///|
fn cmp_str(a : String, b : String) -> Int {
  a.lexical_compare(b)
}

///|
fn cmp_int(a : Int, b : Int) -> Int {
  if a < b {
    -1
  } else if a > b {
    1
  } else {
    0
  }
}

///|
fn apply_text_case(text : String, mode : String) -> String {
  if text == "" || mode == "" {
    return text
  }
  match mode {
    "lowercase" => text.to_lower()
    "uppercase" => text.to_upper()
    "capitalize-first" => capitalize_first(text)
    "capitalize-all" => capitalize_all(text)
    "title" => title_case(text)
    "sentence" => sentence_case(text)
    _ => text
  }
}

///|
fn capitalize_first(text : String) -> String {
  let chars = text.to_array()
  if chars.is_empty() {
    return text
  }
  let mut i = 0
  while i < chars.length() && is_ws(chars[i]) {
    i = i + 1
  }
  if i < chars.length() {
    chars[i] = chars[i].to_ascii_uppercase()
  }
  String::from_array(chars)
}

///|
fn capitalize_all(text : String) -> String {
  let chars = text.to_array()
  let mut start = true
  for i = 0; i < chars.length(); i = i + 1 {
    if is_ws(chars[i]) || chars[i] == '-' {
      start = true
    } else if start {
      chars[i] = chars[i].to_ascii_uppercase()
      start = false
    }
  }
  String::from_array(chars)
}

///|
fn sentence_case(text : String) -> String {
  capitalize_first(text.to_lower())
}

///|
fn title_small(word : String) -> Bool {
  let w = word.to_lower()
  w == "a" ||
  w == "an" ||
  w == "and" ||
  w == "as" ||
  w == "at" ||
  w == "but" ||
  w == "by" ||
  w == "for" ||
  w == "in" ||
  w == "nor" ||
  w == "of" ||
  w == "on" ||
  w == "or" ||
  w == "the" ||
  w == "to" ||
  w == "up" ||
  w == "via"
}

///|
fn title_case(text : String) -> String {
  let out = StringBuilder()
  let word = StringBuilder()
  let mut first = true
  fn flush(first_word : Bool) -> Unit {
    let w = word.to_string()
    word.reset()
    if w == "" {
      return
    }
    if !first_word && title_small(w) {
      out.write_string(w.to_lower())
    } else {
      out.write_string(capitalize_first(w.to_lower()))
    }
  }

  for c in text {
    if is_ws(c) || c == '-' || c == '/' {
      flush(first)
      first = false
      out.write_char(c)
    } else {
      word.write_char(c)
    }
  }
  flush(first)
  out.to_string()
}

///|
fn strip_periods(text : String) -> String {
  let out = StringBuilder()
  for c in text {
    if c != '.' {
      out.write_char(c)
    }
  }
  out.to_string()
}

///|
fn pad2(n : Int) -> String {
  if n >= 0 && n < 10 {
    "0" + n.to_string()
  } else {
    n.to_string()
  }
}

///|
fn year_letter(index : Int) -> String {
  if index < 0 {
    return ""
  }
  // a, b, ... z, aa, ab ...
  let mut n = index
  let mut chars : Array[Char] = []
  while true {
    chars.push(Int::unsafe_to_char(97 + n % 26))
    n = n / 26 - 1
    if n < 0 {
      break
    }
  }
  chars = chars.rev()
  String::from_array(chars)
}