///|
fn hex_digit(byte : Byte) -> Int32 raise NrrdError {
  let n = byte.to_int()
  if n >= 48 && n <= 57 {
    return n - 48
  }
  if n >= 65 && n <= 70 {
    return n - 55
  }
  if n >= 97 && n <= 102 {
    return n - 87
  }
  raise Invalid("invalid hexadecimal digit")
}

///|
fn decode_hex(
  payload : Bytes,
  expected : Int32,
  limits : Limits,
) -> Bytes raise NrrdError {
  if payload.length() > limits.max_payload_bytes {
    raise Limit("hex encoded payload budget")
  }
  let result = FixedArray::make(expected, b'\x00')
  let mut high : Int32? = None
  let mut used = 0
  for b in payload {
    if b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' {
      continue
    }
    let d = hex_digit(b)
    match high {
      None => high = Some(d)
      Some(h) => {
        if used >= expected {
          raise Invalid("extra hex bytes")
        }
        result[used] = (h * 16 + d).to_byte()
        used = used + 1
        high = None
      }
    }
  }
  if high != None {
    raise Invalid("odd hex digit count")
  }
  if used != expected {
    raise Truncated("hex payload")
  }
  Bytes::from_array(result[:])
}

///|
fn encode_hex(raw : Bytes, limits : Limits) -> Bytes raise NrrdError {
  if raw.length() > limits.max_payload_bytes / 2 {
    raise Limit("hex output budget")
  }
  let alphabet = b"0123456789abcdef"
  Bytes::makei(raw.length() * 2, fn(i) {
    let byte = raw[i / 2].to_int()
    alphabet[if i % 2 == 0 { byte >> 4 } else { byte & 15 }]
  })
}