///|
priv suberror OdError {
  OdError(String)
}

///|
struct Format {
  kind : Char
  size : Int
  trailer : Bool
}

///|
fn number(
  value : UInt64,
  radix : Int,
  width : Int,
  fill? : Char = '0',
) -> String {
  let digits = "0123456789abcdef"
  let out : Array[Char] = []
  let mut n = value
  for ;; {
    out.push(digits[(n % radix.to_uint64()).to_int()].to_int().unsafe_to_char())
    n /= radix.to_uint64()
    if n == 0UL {
      break
    }
  }
  while out.length() < width {
    out.push(fill)
  }
  let sb = StringBuilder()
  for i = out.length() - 1; i >= 0; i = i - 1 {
    sb.write_char(out[i])
  }
  sb.to_string()
}

///|
fn spaces(count : Int) -> String {
  let sb = StringBuilder()
  for _ in 0.. Array[Format] raise OdError {
  let result : Array[Format] = []
  let mut i = 0
  while i < text.length() {
    let kind = text[i].to_int().unsafe_to_char()
    if kind != 'a' &&
      kind != 'c' &&
      kind != 'd' &&
      kind != 'u' &&
      kind != 'o' &&
      kind != 'x' {
      raise OdError(
        "unsupported format '\{kind}'; use a, c, or integer d/u/o/x formats",
      )
    }
    i += 1
    let mut size = if kind == 'a' || kind == 'c' { 1 } else { 4 }
    if kind != 'a' && kind != 'c' && i < text.length() {
      match text[i] {
        '1' | 'C' => {
          size = 1
          i += 1
        }
        '2' | 'S' => {
          size = 2
          i += 1
        }
        '4' | 'I' => {
          size = 4
          i += 1
        }
        '8' | 'L' => {
          size = 8
          i += 1
        }
        _ => ()
      }
    }
    let trailer = i < text.length() && text[i] == 'z'
    if trailer {
      i += 1
    }
    result.push({ kind, size, trailer, })
  }
  if result.is_empty() {
    raise OdError("empty output format")
  }
  result
}

///|
fn Format::width(self : Format) -> Int {
  match self.kind {
    'a' | 'c' => 3
    'x' => self.size * 2
    'o' => (self.size * 8 + 2) / 3
    'u' =>
      match self.size {
        1 => 3
        2 => 5
        4 => 10
        _ => 20
      }
    _ =>
      match self.size {
        1 => 4
        2 => 6
        4 => 11
        _ => 20
      }
  }
}

///|
fn character(byte : Byte, named : Bool) -> String {
  let names = [
    "nul", "soh", "stx", "etx", "eot", "enq", "ack", "bel", "bs", "ht", "nl", "vt",
    "ff", "cr", "so", "si", "dle", "dc1", "dc2", "dc3", "dc4", "nak", "syn", "etb",
    "can", "em", "sub", "esc", "fs", "gs", "rs", "us", "sp",
  ]
  let code = if named { byte.to_int() & 127 } else { byte.to_int() }
  let text = if named && code <= 32 {
    names[code]
  } else if named && code == 127 {
    "del"
  } else if code >= 32 && code <= 126 {
    code.unsafe_to_char().to_string()
  } else {
    match code {
      0 => "\\0"
      7 => "\\a"
      8 => "\\b"
      9 => "\\t"
      10 => "\\n"
      11 => "\\v"
      12 => "\\f"
      13 => "\\r"
      _ => number(code.to_uint64(), 8, 3)
    }
  }
  spaces(3 - text.length()) + text
}

///|
fn value_at(data : Bytes, start : Int, size : Int, big : Bool) -> UInt64 {
  let mut result = 0UL
  for i in 0.. String {
  if self.kind == 'a' || self.kind == 'c' {
    return character(data[start], self.kind == 'a')
  }
  let value = value_at(data, start, self.size, big)
  let width = self.width()
  match self.kind {
    'x' => number(value, 16, width)
    'o' => number(value, 8, width)
    'u' => number(value, 10, width, fill=' ')
    _ => {
      let signed = if self.size == 8 {
        value.reinterpret_as_int64()
      } else {
        let bits = self.size * 8
        if (value & (1UL << (bits - 1))) != 0UL {
          value.reinterpret_as_int64() - (1L << bits)
        } else {
          value.reinterpret_as_int64()
        }
      }
      let text = signed.to_string()
      spaces(width - text.length()) + text
    }
  }
}

///|
fn address(offset : Int64, base : Int) -> String {
  if base == 0 {
    ""
  } else {
    number(offset.reinterpret_as_uint64(), base, if base == 16 { 6 } else { 7 })
  }
}

///|
fn render_row(
  data : Bytes,
  offset : Int64,
  width : Int,
  formats : Array[Format],
  base : Int,
  big : Bool,
) -> String {
  let mut line_width = 0
  for format in formats {
    line_width = line_width.max((format.width() + 1) * (width / format.size))
  }
  let out = StringBuilder()
  for index, format in formats {
    let prefix = address(offset, base)
    out.write_string(if index == 0 { prefix } else { spaces(prefix.length()) })
    let fields = width / format.size
    let padding = line_width - fields * format.width()
    let count = (data.length() + format.size - 1) / format.size
    for field in 0..")
      for byte in data {
        out.write_char(
          if byte >= b' ' && byte <= b'~' {
            byte.to_int().unsafe_to_char()
          } else {
            '.'
          },
        )
      }
      out.write_char('<')
    }
    out.write_char('\n')
  }
  out.to_string()
}