///|
/// True for an ASCII decimal character.
pub fn ascii_digit(c : UInt16) -> Bool {
  c >= '0' && c <= '9'
}

///|
/// True for an ASCII alphabetic character.
pub fn ascii_alpha(c : UInt16) -> Bool {
  (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
}

///|
/// True for an ASCII alphanumeric character.
pub fn ascii_alnum(c : UInt16) -> Bool {
  ascii_digit(c) || ascii_alpha(c)
}

///|
/// Convert a hexadecimal character to its nibble value.
fn hex_nibble(c : UInt16) -> Int? {
  if c >= '0' && c <= '9' {
    Some(c.to_int() - 48)
  } else if c >= 'A' && c <= 'F' {
    Some(c.to_int() - 55)
  } else if c >= 'a' && c <= 'f' {
    Some(c.to_int() - 87)
  } else {
    None
  }
}

///|
/// Convert one nibble to uppercase hexadecimal.
fn nibble_char(value : Int) -> Char {
  if value < 10 {
    (48 + value).unsafe_to_char()
  } else {
    (55 + value).unsafe_to_char()
  }
}

///|
/// Copy a byte range without exposing mutable backing storage.
fn bytes_slice(data : Bytes, start : Int, end : Int) -> Bytes {
  Bytes::makei(end - start, i => data[start + i])
}

///|
/// Append immutable bytes into a mutable byte array.
fn append_bytes(target : Array[Byte], source : Bytes) -> Unit {
  for byte in source {
    target.push(byte)
  }
}

///|
/// Concatenate two byte sequences.
pub fn concat_bytes(left : Bytes, right : Bytes) -> Bytes {
  let out : Array[Byte] = []
  append_bytes(out, left)
  append_bytes(out, right)
  Bytes::from_array(out)
}

///|
/// Convert ASCII text into bytes, rejecting non-ASCII characters.
pub fn text_to_ascii(field : Int, text : String) -> Result[Bytes, IsoError] {
  let out : Array[Byte] = []
  for i = 0; i < text.length(); i = i + 1 {
    let value = text[i].to_int()
    if value < 0 || value > 127 {
      return Err(InvalidCharacter(field, i, "non-ASCII"))
    }
    out.push(value.to_byte())
  }
  Ok(Bytes::from_array(out))
}

///|
/// Convert bytes to an ASCII string, rejecting bytes above 0x7f.
pub fn ascii_to_text(field : Int, data : Bytes) -> Result[String, IsoError] {
  let out = StringBuilder()
  for i = 0; i < data.length(); i = i + 1 {
    let value = data[i].to_int()
    if value > 127 {
      return Err(InvalidCharacter(field, i, "non-ASCII byte"))
    }
    out.write_char(value.unsafe_to_char())
  }
  Ok(out.to_string())
}

///|
/// Encode bytes as uppercase hexadecimal.
pub fn hex_encode(data : Bytes) -> String {
  let out = StringBuilder()
  for byte in data {
    let value = byte.to_int()
    out.write_char(nibble_char((value >> 4) & 15))
    out.write_char(nibble_char(value & 15))
  }
  out.to_string()
}

///|
/// Decode even-length hexadecimal text.
pub fn hex_decode(text : String) -> Result[Bytes, IsoError] {
  if text.length() % 2 != 0 {
    return Err(InvalidHex(text))
  }
  let out : Array[Byte] = []
  let mut i = 0
  while i < text.length() {
    match (hex_nibble(text[i]), hex_nibble(text[i + 1])) {
      (Some(high), Some(low)) => out.push(((high << 4) | low).to_byte())
      _ => return Err(InvalidHex(text))
    }
    i += 2
  }
  Ok(Bytes::from_array(out))
}

///|
/// Render a non-negative integer as zero-padded decimal text.
pub fn decimal_width(value : Int, width : Int) -> Result[String, IsoError] {
  if value < 0 {
    return Err(LengthPrefixOverflow(width, value))
  }
  let text = value.to_string()
  if text.length() > width {
    return Err(LengthPrefixOverflow(width, value))
  }
  let out = StringBuilder()
  for _ in 0..<(width - text.length()) {
    out.write_char('0')
  }
  out.write_string(text)
  Ok(out.to_string())
}

///|
/// Parse a decimal ASCII range without allocating an intermediate slice.
pub fn parse_decimal_bytes(
  data : Bytes,
  start : Int,
  width : Int,
) -> Result[Int, IsoError] {
  if start < 0 || width < 0 || start + width > data.length() {
    return Err(
      Truncated(
        "decimal length prefix",
        width,
        Int::max(0, data.length() - start),
      ),
    )
  }
  let mut value = 0
  for i = start; i < start + width; i = i + 1 {
    let digit = data[i].to_int() - 48
    if digit < 0 || digit > 9 {
      return Err(
        InvalidNumeric(0, hex_encode(bytes_slice(data, start, start + width))),
      )
    }
    value = value * 10 + digit
  }
  Ok(value)
}

///|
/// Left pad text to a fixed width.
pub fn left_pad(text : String, width : Int, fill : UInt16) -> String {
  if text.length() >= width {
    return text
  }
  let out = StringBuilder()
  for _ in 0..<(width - text.length()) {
    out.write_char(fill.to_int().unsafe_to_char())
  }
  out.write_string(text)
  out.to_string()
}

///|
/// Right pad text to a fixed width.
pub fn right_pad(text : String, width : Int, fill : UInt16) -> String {
  if text.length() >= width {
    return text
  }
  let out = StringBuilder()
  out.write_string(text)
  for _ in 0..<(width - text.length()) {
    out.write_char(fill.to_int().unsafe_to_char())
  }
  out.to_string()
}

///|
/// Remove leading padding down to a minimum of one character.
pub fn trim_left_char(text : String, fill : UInt16) -> String {
  let mut i = 0
  while i + 1 < text.length() && text[i] == fill {
    i += 1
  }
  text[i:].to_owned()
}

///|
/// Remove trailing padding.
pub fn trim_right_char(text : String, fill : UInt16) -> String {
  let mut end = text.length()
  while end > 0 && text[end - 1] == fill {
    end -= 1
  }
  text[0:end].to_owned()
}

///|
/// Constant-time-ish equality for byte strings of equal public length.
pub fn bytes_equal(left : Bytes, right : Bytes) -> Bool {
  if left.length() != right.length() {
    return false
  }
  let mut diff = 0
  for i = 0; i < left.length(); i = i + 1 {
    diff = diff | (left[i].to_int() ^ right[i].to_int())
  }
  diff == 0
}