///|
/// Build a string from a character array without relying on a particular
/// backend's string representation.
pub fn chars_to_string(chars : Array[Char]) -> String {
  let builder = StringBuilder()
  for c in chars {
    builder.write_char(c)
  }
  builder.to_string()
}

///|
pub fn char_count(text : String) -> Int {
  text.char_length()
}

///|
pub fn code_unit_count(text : String) -> Int {
  text.length()
}

///|
pub fn is_ascii_char(c : Char) -> Bool {
  c.is_ascii()
}

///|
pub fn is_ascii_digit_char(c : Char) -> Bool {
  c.is_ascii_digit()
}

///|
pub fn is_ascii_letter_char(c : Char) -> Bool {
  c.is_ascii_alphabetic()
}

///|
pub fn is_ascii_alphanumeric_char(c : Char) -> Bool {
  c.is_ascii_digit() || c.is_ascii_alphabetic()
}

///|
pub fn is_hex_char(c : Char) -> Bool {
  c.is_ascii_hexdigit()
}

///|
pub fn is_word_char(c : Char) -> Bool {
  is_ascii_alphanumeric_char(c) || c == '_' || c.is_numeric()
}

///|
pub fn is_cjk_char(c : Char) -> Bool {
  let value = c.to_int()
  (value >= 0x3400 && value <= 0x4DBF) ||
  (value >= 0x4E00 && value <= 0x9FFF) ||
  (value >= 0xF900 && value <= 0xFAFF)
}

///|
pub fn is_blank_char(c : Char) -> Bool {
  c.is_whitespace() || c == '\u{3000}'
}

///|
pub fn is_ascii_punctuation_char(c : Char) -> Bool {
  c.is_ascii_punctuation()
}

///|
pub fn all_ascii_digits(text : String) -> Bool {
  if text.is_empty() {
    false
  } else {
    text.to_array().all(is_ascii_digit_char)
  }
}

///|
pub fn all_ascii_hex(text : String) -> Bool {
  if text.is_empty() {
    false
  } else {
    text.to_array().all(is_hex_char)
  }
}

///|
pub fn contains_cjk(text : String) -> Bool {
  text.to_array().any(is_cjk_char)
}

///|
pub fn contains_ascii_letter(text : String) -> Bool {
  text.to_array().any(is_ascii_letter_char)
}

///|
pub fn trim_ascii_space(text : String) -> String {
  text.trim(chars=" \t\r\n").to_owned()
}

///|
pub fn collapse_spaces(text : String) -> String {
  let builder = StringBuilder()
  let mut pending_space = false
  for c in text {
    if is_blank_char(c) {
      pending_space = true
    } else {
      if pending_space && !builder.is_empty() {
        builder.write_char(' ')
      }
      builder.write_char(c)
      pending_space = false
    }
  }
  builder.to_string()
}

///|
pub fn normalize_punctuation(text : String) -> String {
  let builder = StringBuilder()
  for c in text {
    match c {
      ':' => builder.write_char(':')
      ',' => builder.write_char(',')
      ';' => builder.write_char(';')
      '(' => builder.write_char('(')
      ')' => builder.write_char(')')
      '[' => builder.write_char('[')
      ']' => builder.write_char(']')
      '-' | '—' | '–' => builder.write_char('-')
      '/' => builder.write_char('/')
      '.' => builder.write_char('.')
      '@' => builder.write_char('@')
      _ => builder.write_char(c)
    }
  }
  builder.to_string()
}

///|
pub fn normalize_for_matching(text : String) -> String {
  collapse_spaces(normalize_punctuation(text))
}

///|
pub fn keep_digits(text : String) -> String {
  let builder = StringBuilder()
  for c in text {
    if c.is_ascii_digit() {
      builder.write_char(c)
    }
  }
  builder.to_string()
}

///|
pub fn keep_ascii_letters_and_digits(text : String) -> String {
  let builder = StringBuilder()
  for c in text {
    if is_ascii_alphanumeric_char(c) {
      builder.write_char(c)
    }
  }
  builder.to_string()
}

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

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

///|
pub fn repeated_char(c : Char, count : Int) -> String {
  if count <= 0 {
    ""
  } else {
    String::make(count, c)
  }
}

///|
pub fn safe_char_slice(text : String, start : Int, end : Int) -> String {
  let chars = text.to_array()
  let lower = if start < 0 {
    0
  } else if start > chars.length() {
    chars.length()
  } else {
    start
  }
  let upper = if end < lower {
    lower
  } else if end > chars.length() {
    chars.length()
  } else {
    end
  }
  let builder = StringBuilder()
  for i in lower.. Char? {
  text.to_array().get(0)
}

///|
pub fn last_char(text : String) -> Char? {
  let chars = text.to_array()
  if chars.is_empty() {
    None
  } else {
    chars.get(chars.length() - 1)
  }
}

///|
pub fn char_at_or(text : String, position : Int, fallback : Char) -> Char {
  match text.to_array().get(position) {
    Some(c) => c
    None => fallback
  }
}

///|
pub fn starts_with_word(text : String, prefix : String) -> Bool {
  if !text.has_prefix(prefix) {
    false
  } else if text.length() == prefix.length() {
    true
  } else {
    match text.get_char(prefix.length()) {
      Some(c) => !is_word_char(c)
      None => true
    }
  }
}

///|
pub fn ends_with_word(text : String, suffix : String) -> Bool {
  if !text.has_suffix(suffix) {
    false
  } else if text.length() == suffix.length() {
    true
  } else {
    match text.get_char(text.length() - suffix.length() - 1) {
      Some(c) => !is_word_char(c)
      None => true
    }
  }
}

///|
pub fn boundary_before(text : String, position : Int) -> Bool {
  if position <= 0 {
    true
  } else {
    match text.get_char(position - 1) {
      Some(c) => !is_word_char(c)
      None => true
    }
  }
}

///|
pub fn boundary_after(text : String, position : Int) -> Bool {
  if position >= text.length() {
    true
  } else {
    match text.get_char(position) {
      Some(c) => !is_word_char(c)
      None => true
    }
  }
}

///|
pub fn is_word_boundary(text : String, start : Int, end : Int) -> Bool {
  boundary_before(text, start) && boundary_after(text, end)
}

///|
pub fn line_count(text : String) -> Int {
  if text.is_empty() {
    0
  } else {
    let mut count = 1
    for c in text {
      if c == '\n' {
        count += 1
      }
    }
    count
  }
}

///|
pub fn line_column(text : String, offset : Int) -> LineColumn {
  let bounded = if offset < 0 {
    0
  } else if offset > text.length() {
    text.length()
  } else {
    offset
  }
  let mut line = 1
  let mut column = 1
  for i in 0.. TextWindow {
  let safe_radius = if radius < 0 { 0 } else { radius }
  let span = Span::{ start: center - safe_radius, end: center + safe_radius }.clamp(
    text.length(),
  )
  {
    start: span.start,
    end: span.end,
    text: text[span.start:span.end].to_owned(),
  }
}

///|
pub fn split_lines_with_offsets(text : String) -> Array[TextWindow] {
  let windows = []
  let mut start = 0
  for i in 0.. String {
  let missing = width - text.char_length()
  if missing <= 0 {
    text
  } else {
    repeated_char(fill, missing) + text
  }
}

///|
pub fn pad_right(text : String, width : Int, fill : Char) -> String {
  let missing = width - text.char_length()
  if missing <= 0 {
    text
  } else {
    text + repeated_char(fill, missing)
  }
}

///|
pub fn quote_for_log(text : String) -> String {
  let builder = StringBuilder()
  builder.write_char('"')
  for c in text {
    match c {
      '"' => builder.write_string("\\\"")
      '\\' => builder.write_string("\\\\")
      '\n' => builder.write_string("\\n")
      '\r' => builder.write_string("\\r")
      '\t' => builder.write_string("\\t")
      _ => builder.write_char(c)
    }
  }
  builder.write_char('"')
  builder.to_string()
}

///|
pub fn numeric_suffix(text : String) -> String {
  let chars = text.to_array()
  let mut start = chars.length()
  while start > 0 && chars[start - 1].is_ascii_digit() {
    start -= 1
  }
  let builder = StringBuilder()
  for i in start.. String {
  let chars = text.to_array()
  let mut end = 0
  while end < chars.length() && chars[end].is_ascii_digit() {
    end += 1
  }
  let builder = StringBuilder()
  for i in 0.. Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for c in text {
    let key = c.to_string()
    counts[key] = counts.get_or_default(key, 0) + 1
  }
  counts
}

///|
pub fn count_char(text : String, target : Char) -> Int {
  let mut result = 0
  for c in text {
    if c == target {
      result += 1
    }
  }
  result
}

///|
pub fn count_substring(text : String, needle : String) -> Int {
  if needle.is_empty() {
    0
  } else {
    let mut count = 0
    let mut cursor = 0
    while cursor + needle.length() <= text.length() {
      if text[cursor:cursor + needle.length()] == needle {
        count += 1
        cursor += needle.length()
      } else {
        cursor += 1
      }
    }
    count
  }
}

///|
pub fn longest_common_prefix(left : String, right : String) -> String {
  let limit = if left.length() < right.length() {
    left.length()
  } else {
    right.length()
  }
  let mut end = 0
  while end < limit && left[end] == right[end] {
    end += 1
  }
  left[:end].to_owned()
}

///|
pub fn redactable_char_count(text : String) -> Int {
  text.to_array().filter(fn(c) { !is_blank_char(c) }).length()
}