// Small text helpers shared by the codecs. All codecs work on ASCII, but the
// helpers are written over `Char` so a stray non-ASCII character is rejected
// rather than sliced in the middle of a UTF-8 sequence.

///|
/// The characters of a string, in order.
pub fn chars_of(s : String) -> Array[Char] {
  let out : Array[Char] = []
  for ch in s {
    out.push(ch)
  }
  out
}

///|
/// Build a string from characters.
pub fn string_of(chars : Array[Char]) -> String {
  let buf = StringBuilder()
  for ch in chars {
    buf.write_char(ch)
  }
  buf.to_string()
}

///|
/// True when every character is ASCII (code point below 128).
pub fn is_ascii(s : String) -> Bool {
  for ch in s {
    if ch.to_int() >= 128 {
      return false
    }
  }
  true
}

///|
/// ASCII uppercase for the codecs' alphabets: digits, `+`, `-`, `.`, space and
/// letters are the only inputs that matter.
pub fn ascii_upper_char(c : Char) -> Char {
  if c >= 'a' && c <= 'z' {
    (c.to_int() - 32).unsafe_to_char()
  } else {
    c
  }
}

///|
/// Uppercase a string using ASCII rules only.
pub fn ascii_upper(s : String) -> String {
  let out : Array[Char] = []
  for ch in s {
    out.push(ascii_upper_char(ch))
  }
  string_of(out)
}

///|
/// Lowercase a string using ASCII rules only.
pub fn ascii_lower(s : String) -> String {
  let out : Array[Char] = []
  for ch in s {
    if ch >= 'A' && ch <= 'Z' {
      out.push((ch.to_int() + 32).unsafe_to_char())
    } else {
      out.push(ch)
    }
  }
  string_of(out)
}

///|
/// A one-character string.
pub fn char_str(c : Char) -> String {
  let buf = StringBuilder()
  buf.write_char(c)
  buf.to_string()
}

///|
/// The index of `needle` in `chars`, or -1.
pub fn index_of(chars : Array[Char], needle : Char) -> Int {
  let mut i = 0
  while i < chars.length() {
    if chars[i] == needle {
      return i
    }
    i = i + 1
  }
  -1
}

///|
/// Remove leading and trailing ASCII whitespace.
pub fn trim_ascii(s : String) -> String {
  let chars = chars_of(s)
  let mut start = 0
  let mut end = chars.length()
  while start < end && is_space(chars[start]) {
    start = start + 1
  }
  while end > start && is_space(chars[end - 1]) {
    end = end - 1
  }
  let out : Array[Char] = []
  for i = start; i < end; i = i + 1 {
    out.push(chars[i])
  }
  string_of(out)
}

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

///|
/// The digit value of an ASCII character, or `None`.
pub fn digit_value(c : Char) -> Int? {
  if c >= '0' && c <= '9' {
    Some(c.to_int() - 48)
  } else {
    None
  }
}

///|
/// Parse a plain decimal number without exceptions: `[+-]?digits[.digits]`.
/// Coordinate text never needs an exponent, and refusing one keeps the parser
/// total and predictable.
pub fn parse_decimal(text : String) -> Double? {
  let chars = chars_of(trim_ascii(text))
  let n = chars.length()
  if n == 0 {
    return None
  }
  let mut i = 0
  let mut sign = 1.0
  if chars[i] == '-' || chars[i] == '+' {
    if chars[i] == '-' {
      sign = -1.0
    }
    i = i + 1
  }
  let mut int_part = 0.0
  let mut digits = 0
  while i < n && digit_value(chars[i]) is Some(d) {
    int_part = int_part * 10.0 + d.to_double()
    digits = digits + 1
    i = i + 1
  }
  let mut frac = 0.0
  let mut factor = 0.1
  if i < n && chars[i] == '.' {
    i = i + 1
    while i < n && digit_value(chars[i]) is Some(d) {
      frac = frac + d.to_double() * factor
      factor = factor / 10.0
      digits = digits + 1
      i = i + 1
    }
  }
  if digits == 0 || i != n {
    return None
  }
  Some(sign * (int_part + frac))
}

///|
/// Split on ASCII whitespace, dropping empty fields.
pub fn split_whitespace(text : String) -> Array[String] {
  let chars = chars_of(trim_ascii(text))
  let out : Array[String] = []
  let mut current : Array[Char] = []
  for ch in chars {
    if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' {
      if current.length() > 0 {
        out.push(string_of(current))
        current = []
      }
    } else {
      current.push(ch)
    }
  }
  if current.length() > 0 {
    out.push(string_of(current))
  }
  out
}

///|
/// Drop trailing zeros (and a trailing point) from a fixed-point rendering.
pub fn trim_fraction(text : String) -> String {
  let chars = chars_of(text)
  let mut end = chars.length()
  while end > 0 && chars[end - 1] == '0' {
    end = end - 1
  }
  if end > 0 && chars[end - 1] == '.' {
    end = end - 1
  }
  let out : Array[Char] = []
  for i = 0; i < end; i = i + 1 {
    out.push(chars[i])
  }
  string_of(out)
}