///|
/// Typed standard binary KV1. Floating-point payloads retain their exact bits,
/// including NaN payloads and negative zero. Pointer values are data, never dereferenced.
pub(all) enum BinaryValue {
  ObjectValue(Array[BinaryEntry])
  StringValue(String)
  Int32Value(Int)
  Float32Bits(UInt)
  Pointer32(UInt)
  WideString(String)
  Color32(UInt)
  UInt64Value(UInt64)
  Int64Value(Int64)
} derive(Eq, Debug)

///|
pub(all) struct BinaryEntry {
  key : String
  value : BinaryValue
} derive(Eq, Debug)

///|
pub(all) struct BinaryOptions {
  max_depth : Int
  max_entries : Int
  max_bytes : Int
  max_string_bytes : Int
} derive(Eq, Debug)

///|
pub fn BinaryOptions::default() -> BinaryOptions {
  {
    max_depth: 128,
    max_entries: 100000,
    max_bytes: 16777216,
    max_string_bytes: 1048576,
  }
}

///|
pub suberror BinaryError {
  InvalidBinary(Int, String)
} derive(Debug)

///|
pub extend BinaryValue with Eq::{equal, not_equal}

///|
pub extend BinaryValue with @debug.Debug::{to_repr}

///|
pub extend BinaryEntry with Eq::{equal, not_equal}

///|
pub extend BinaryEntry with @debug.Debug::{to_repr}

///|
pub extend BinaryOptions with Eq::{equal, not_equal}

///|
pub extend BinaryOptions with @debug.Debug::{to_repr}

///|
pub extend BinaryError with @debug.Debug::{to_repr}

///|
fn check_binary_options(options : BinaryOptions) -> Unit raise BinaryError {
  if options.max_depth < 0 ||
    options.max_depth > 128 ||
    options.max_entries < 0 ||
    options.max_bytes < 1 ||
    options.max_string_bytes < 0 {
    raise InvalidBinary(0, "Invalid binary resource limits")
  }
}

///|
priv struct BinaryReader {
  bytes : Bytes
  options : BinaryOptions
  mut pos : Int
  mut count : Int
  table : Array[String]?
}

///|
/// Decode standard KV1 with marker 8. Reject truncated streams, trailing bytes,
/// invalid encodings and unknown types. Appinfo containers and VBKV are separate formats.
pub fn decode_binary(
  bytes : Bytes,
  options? : BinaryOptions = BinaryOptions::default(),
  string_table? : Array[String]? = None,
) -> Array[BinaryEntry] raise BinaryError {
  check_binary_options(options)
  if bytes.length() > options.max_bytes {
    raise InvalidBinary(0, "Input exceeds byte limit")
  }
  validate_string_table(string_table, options) |> ignore
  let reader = BinaryReader::{
    bytes,
    options,
    pos: 0,
    count: 0,
    table: string_table,
  }
  let entries = reader.read_binary_entries(0)
  if reader.pos != bytes.length() {
    raise InvalidBinary(reader.pos, "Trailing bytes after root terminator")
  }
  entries
}

///|
fn BinaryReader::byte(self : BinaryReader) -> Int raise BinaryError {
  guard self.bytes.get(self.pos) is Some(value) else {
    raise InvalidBinary(self.pos, "Unexpected end of binary input")
  }
  self.pos += 1
  value.to_int()
}

///|
fn BinaryReader::number(
  self : BinaryReader,
  width : Int,
) -> UInt64 raise BinaryError {
  let mut value = 0UL
  for i in 0.. String raise BinaryError {
  let start = self.pos
  let width = if wide { 2 } else { 1 }
  while true {
    let first = self.byte()
    let second = if wide { self.byte() } else { 0 }
    if first == 0 && second == 0 {
      break
    }
    if self.pos - start > self.options.max_string_bytes {
      raise InvalidBinary(start, "String exceeds byte limit")
    }
  }
  let view = self.bytes[start:self.pos - width]
  try {
    if wide {
      @utf16.decode(view, ignore_bom=false, endianness=@utf16.Little)
    } else {
      @utf8.decode(view, ignore_bom=false)
    }
  } catch {
    _ => raise InvalidBinary(start, "Malformed string encoding")
  }
}

///|
fn BinaryReader::read_binary_entries(
  self : BinaryReader,
  depth : Int,
) -> Array[BinaryEntry] raise BinaryError {
  let entries : Array[BinaryEntry] = []
  while true {
    let offset = self.pos
    let tag = self.byte()
    if tag == 8 {
      return entries
    }
    if tag < 0 || tag > 10 || tag == 8 || tag == 9 {
      raise InvalidBinary(offset, "Unknown binary type")
    }
    if self.count >= self.options.max_entries {
      raise InvalidBinary(offset, "Entry limit exceeded")
    }
    self.count += 1
    let key = match self.table {
      None => self.cstring(false)
      Some(table) => {
        let index = self.number(4)
        if index >= table.length().to_uint64() {
          raise InvalidBinary(
            self.pos - 4,
            "String table index is out of range",
          )
        }
        table[index.to_int()]
      }
    }
    let value = match tag {
      0 => {
        if depth >= self.options.max_depth {
          raise InvalidBinary(offset, "Object depth limit exceeded")
        }
        BinaryValue::ObjectValue(self.read_binary_entries(depth + 1))
      }
      1 => StringValue(self.cstring(false))
      2 => Int32Value(self.number(4).to_uint().reinterpret_as_int())
      3 => Float32Bits(self.number(4).to_uint())
      4 => Pointer32(self.number(4).to_uint())
      5 => WideString(self.cstring(true))
      6 => Color32(self.number(4).to_uint())
      7 => UInt64Value(self.number(8))
      10 => Int64Value(self.number(8).reinterpret_as_int64())
      _ => raise InvalidBinary(offset, "Unknown binary type")
    }
    entries.push({ key, value, })
  }
  entries
}

///|
priv struct BinaryWriter {
  buffer : @buffer.Buffer
  options : BinaryOptions
  mut count : Int
  table : Map[String, Int]?
}

///|
fn BinaryWriter::check_size(
  self : BinaryWriter,
  addition : Int,
) -> Unit raise BinaryError {
  if addition > self.options.max_bytes - self.buffer.length() {
    raise InvalidBinary(self.buffer.length(), "Output exceeds byte limit")
  }
}

///|
fn BinaryWriter::string(
  self : BinaryWriter,
  text : String,
  wide : Bool,
) -> Unit raise BinaryError {
  if text.char_length() > self.options.max_string_bytes ||
    text.contains("\u0000") {
    raise InvalidBinary(
      self.buffer.length(),
      "String exceeds limit or contains NUL",
    )
  }
  let bytes = if wide {
    @utf16.encode(text[:], endianness=@utf16.Little)
  } else {
    @utf8.encode(text[:])
  }
  if bytes.length() > self.options.max_string_bytes {
    raise InvalidBinary(self.buffer.length(), "String exceeds byte limit")
  }
  self.check_size(bytes.length() + (if wide { 2 } else { 1 }))
  self.buffer.write_bytes(bytes[:])
  self.buffer.write_byte(0)
  if wide {
    self.buffer.write_byte(0)
  }
}

///|
/// Encode with the same limits as decoding. Order, duplicate keys and numeric
/// types survive a decode/encode round trip without coercion through JSON numbers.
pub fn encode_binary(
  entries : Array[BinaryEntry],
  options? : BinaryOptions = BinaryOptions::default(),
  string_table? : Array[String]? = None,
) -> Bytes raise BinaryError {
  check_binary_options(options)
  let table = validate_string_table(string_table, options)
  let writer = BinaryWriter::{
    buffer: @buffer.Buffer(),
    options,
    count: 0,
    table,
  }
  writer.write_binary_entries(entries, 0)
  writer.buffer.to_bytes()
}

///|
fn BinaryWriter::write_binary_entries(
  self : BinaryWriter,
  entries : Array[BinaryEntry],
  depth : Int,
) -> Unit raise BinaryError {
  for entry in entries {
    if self.count >= self.options.max_entries {
      raise InvalidBinary(self.buffer.length(), "Entry limit exceeded")
    }
    self.count += 1
    let tag = match entry.value {
      ObjectValue(_) => 0
      StringValue(_) => 1
      Int32Value(_) => 2
      Float32Bits(_) => 3
      Pointer32(_) => 4
      WideString(_) => 5
      Color32(_) => 6
      UInt64Value(_) => 7
      Int64Value(_) => 10
    }
    self.check_size(1)
    self.buffer.write_byte(tag.to_byte())
    match self.table {
      None => self.string(entry.key, false)
      Some(table) => {
        guard table.get(entry.key) is Some(index) else {
          raise InvalidBinary(
            self.buffer.length(),
            "Key is absent from string table",
          )
        }
        self.check_size(4)
        self.buffer.write_uint_le(index.reinterpret_as_uint())
      }
    }
    match entry.value {
      ObjectValue(children) => {
        if depth >= self.options.max_depth {
          raise InvalidBinary(
            self.buffer.length(),
            "Object depth limit exceeded",
          )
        }
        self.write_binary_entries(children, depth + 1)
      }
      StringValue(s) => self.string(s, false)
      WideString(s) => self.string(s, true)
      Int32Value(n) => {
        self.check_size(4)
        self.buffer.write_int_le(n)
      }
      Float32Bits(n) | Pointer32(n) | Color32(n) => {
        self.check_size(4)
        self.buffer.write_uint_le(n)
      }
      UInt64Value(n) => {
        self.check_size(8)
        self.buffer.write_uint64_le(n)
      }
      Int64Value(n) => {
        self.check_size(8)
        self.buffer.write_int64_le(n)
      }
    }
  }
  self.check_size(1)
  self.buffer.write_byte(8)
}

///|
fn validate_string_table(
  table : Array[String]?,
  options : BinaryOptions,
) -> Map[String, Int]? raise BinaryError {
  guard table is Some(strings) else { return None }
  if strings.length() > 1000000 {
    raise InvalidBinary(0, "String table has too many entries")
  }
  let result : Map[String, Int] = Map([])
  let mut total = 0
  for i in 0.. options.max_string_bytes || text.contains("\u0000") {
      raise InvalidBinary(0, "Invalid string table entry")
    }
    let length = @utf8.encode(text[:]).length()
    if length > options.max_string_bytes || length > options.max_bytes - total {
      raise InvalidBinary(0, "String table exceeds byte budget")
    }
    total += length
    if result.contains(text) {
      raise InvalidBinary(0, "Duplicate string table entry")
    }
    result[text] = i
  }
  Some(result)
}