///|
/// Text and numeric utilities shared by the parser, writer and CLI.
fn byte_length(input : String) -> Int {
  @utf8.encode(input).length()
}

///|
fn strip_one_cr(line : String) -> String {
  if line.has_suffix("\r") {
    let chars = line.to_array()
    String::from_array(chars[0:chars.length() - 1])
  } else {
    line
  }
}

///|
fn contains_illegal_control(input : String) -> Bool {
  for ch in input {
    let value = ch.to_int()
    if (value >= 0 && value < 32 && ch != '\r' && ch != '\n' && ch != '\t') ||
      value == 127 {
      return true
    }
  }
  false
}

///|
fn is_ascii_alpha(ch : Char) -> Bool {
  let value = ch.to_int()
  (value >= 'A'.to_int() && value <= 'Z'.to_int()) ||
  (value >= 'a'.to_int() && value <= 'z'.to_int())
}

///|
fn is_ascii_digit(ch : Char) -> Bool {
  let value = ch.to_int()
  value >= '0'.to_int() && value <= '9'.to_int()
}

///|
fn is_token_char(ch : Char) -> Bool {
  let value = ch.to_int()
  value >= 33 && value <= 126 && ch != ' ' && ch != '=' && ch != ':'
}

///|
fn valid_token(value : String) -> Bool {
  if value.length() == 0 {
    return false
  }
  for ch in value {
    if !is_token_char(ch) {
      return false
    }
  }
  true
}

///|
fn valid_attribute_name(value : String) -> Bool {
  if value.length() == 0 {
    return false
  }
  for ch in value {
    if !(is_ascii_alpha(ch) || is_ascii_digit(ch) || ch == '-' || ch == '_') {
      return false
    }
  }
  true
}

///|
fn parse_non_negative_int(value : String) -> Int? {
  if value.length() == 0 {
    return None
  }
  let mut out = 0
  for ch in value {
    if !is_ascii_digit(ch) {
      return None
    }
    let digit = ch.to_int() - '0'.to_int()
    if out > 214748364 || (out == 214748364 && digit > 7) {
      return None
    }
    out = out * 10 + digit
  }
  Some(out)
}

///|
fn split_compact(value : String, separator : String) -> Array[String] {
  let out : Array[String] = Array::new()
  for item in value.split(separator) {
    let owned = item.to_owned()
    if owned.length() > 0 {
      out.push(owned)
    }
  }
  out
}

///|
fn join_with(items : Array[String], separator : String) -> String {
  let sb = StringBuilder()
  for index, item in items {
    if index > 0 {
      sb.write_string(separator)
    }
    sb.write_string(item)
  }
  sb.to_string()
}

///|
fn ascii_lower(value : String) -> String {
  let chars : Array[Char] = Array::new()
  for ch in value {
    let code = ch.to_int()
    if code >= 'A'.to_int() && code <= 'Z'.to_int() {
      chars.push((code + 32).unsafe_to_char())
    } else {
      chars.push(ch)
    }
  }
  String::from_array(chars)
}

///|
fn string_in(items : Array[String], value : String) -> Bool {
  for item in items {
    if item == value {
      return true
    }
  }
  false
}

///|
fn count_attribute_named(items : Array[Attribute], name : String) -> Int {
  let mut count = 0
  for item in items {
    if item.name().equal_ignore_ascii_case(name) {
      count = count + 1
    }
  }
  count
}

///|
fn json_escape(value : String) -> String {
  let sb = StringBuilder()
  for ch in value {
    match ch {
      '"' => sb.write_string("\\\"")
      '\\' => sb.write_string("\\\\")
      '\n' => sb.write_string("\\n")
      '\r' => sb.write_string("\\r")
      '\t' => sb.write_string("\\t")
      _ => if ch.to_int() >= 32 { sb.write_char(ch) }
    }
  }
  sb.to_string()
}