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

///|
fn words(text : String) -> Array[String] {
  let out : Array[String] = []
  let buffer = StringBuilder()
  for c in text.iter() {
    if whitespace(c) {
      if !buffer.is_empty() {
        out.push(buffer.to_string())
        buffer.reset()
      }
    } else {
      buffer.write_char(c)
    }
  }
  if !buffer.is_empty() {
    out.push(buffer.to_string())
  }
  out
}

///|
/// Strict decimal numbers with optional E exponent; no NaN/Infinity/hex/underscores.
pub fn parse_number(token : String) -> Double raise WellError {
  if words(token).length() != 1 {
    fail("expected one numeric token")
  }
  let chars = token.iter().to_array()
  let n = chars.length()
  let mut i = 0
  if n > 0 && (chars[0] == '+' || chars[0] == '-') {
    i += 1
  }
  let mut digits = 0
  while i < n && chars[i] >= '0' && chars[i] <= '9' {
    i += 1
    digits += 1
  }
  if i < n && chars[i] == '.' {
    i += 1
    while i < n && chars[i] >= '0' && chars[i] <= '9' {
      i += 1
      digits += 1
    }
  }
  if digits == 0 {
    fail("invalid number: " + token)
  }
  if i < n && (chars[i] == 'e' || chars[i] == 'E') {
    i += 1
    if i < n && (chars[i] == '+' || chars[i] == '-') {
      i += 1
    }
    let start = i
    while i < n && chars[i] >= '0' && chars[i] <= '9' {
      i += 1
    }
    if start == i {
      fail("missing exponent digits: " + token)
    }
  }
  if i != n {
    fail("invalid numeric suffix: " + token)
  }
  let value = @string.parse_double(token) catch {
    _ => {
      fail("number conversion failed")
      0.0
    }
  }
  if value == 0.0 {
    let mantissa = token.to_upper().split("E").to_array()[0]
    if mantissa.iter().any(c => c >= '1' && c <= '9') {
      fail("number underflows finite range")
    }
  }
  if !finite(value) {
    fail("number exceeds finite range")
  }
  value
}

///|
/// Parse mnemonic.unit value : description. The last colon separates description (CWLS 2014 section 5.2).
pub fn parse_header(line : String) -> Header raise WellError {
  let (body, description) = match line.rev_split_once(":") {
    Some((a, b)) => (a.to_owned(), b.trim().to_owned())
    None => {
      fail("header requires ':' delimiter")
      ("", "")
    }
  }
  let (left, right) = match body.split_once(".") {
    Some((a, b)) => (a.trim().to_owned(), b.to_owned())
    None => {
      fail("header requires '.' delimiter")
      ("", "")
    }
  }
  if !valid_name(left) {
    fail("invalid header mnemonic: " + left)
  }
  let chars = right.iter().to_array()
  let mut end_unit = 0
  while end_unit < chars.length() && !whitespace(chars[end_unit]) {
    end_unit += 1
  }
  if end_unit == chars.length() {
    fail("header requires space delimiter after unit")
  }
  let unit_buffer = StringBuilder()
  let value_buffer = StringBuilder()
  for i in 0..