///|
priv struct KV3BinaryWriter {
  one : @buffer.Buffer
  two : @buffer.Buffer
  four : @buffer.Buffer
  eight : @buffer.Buffer
  types : @buffer.Buffer
  objects : @buffer.Buffer
  blobs : Array[Bytes]
  names : Array[String]
  strings : Map[String, Int32]
  version : Int32
  options : BinaryOptions
  mut nodes : Int32
  mut arrays : Int32
  mut object_count : Int32
  mut blob_bytes : Int32
  mut scalar_bytes : Int32
}

///|
fn KV3BinaryWriter::string_id(
  self : KV3BinaryWriter,
  value : String,
) -> UInt64 raise BinaryError {
  if value.is_empty() {
    return 0xffffffffUL
  }
  if self.strings.get(value) is Some(id) {
    return id.to_uint64()
  }
  let size = @utf8.encode(value[:]).length()
  if size > self.options.max_string_bytes ||
    value.contains("\u0000") ||
    size + 1 > self.options.max_bytes - self.scalar_bytes {
    raise InvalidBinary(0, "KV3 string exceeds budget or contains NUL")
  }
  if self.names.length() >= self.options.max_entries {
    raise InvalidBinary(0, "KV3 string table exceeds budget")
  }
  self.scalar_bytes += size + 1
  let id = self.names.length()
  self.strings[value] = id
  self.names.push(value)
  id.to_uint64()
}

///|
fn KV3BinaryWriter::value(
  self : KV3BinaryWriter,
  value : KV3BinaryValue,
  depth : Int32,
  flag : String,
) -> Unit raise {
  if value is BFlag(annotation, inner) {
    if !flag.is_empty() || !kv3_flag(annotation) {
      raise InvalidBinary(0, "Invalid or nested KV3 flag")
    }
    self.value(inner, depth, annotation)
    return
  }
  self.nodes += 1
  if depth > self.options.max_depth || self.nodes > self.options.max_entries {
    raise InvalidBinary(0, "KV3 output tree exceeds budget")
  }
  let tag = match value {
    BNull => 1
    BBool(true) => 13
    BBool(false) => 14
    BNumber(kind, _) => {
      kv3_binary_width(kind) |> ignore
      kind
    }
    BString(_) => 6
    BBlob(_) => 7
    BArray(_) => 8
    BObject(_) => 9
    _ => raise InvalidBinary(0, "Invalid binary KV3 value")
  }
  valve_uint(
    self.types,
    (tag | (if flag.is_empty() { 0 } else { 128 })).to_uint64(),
    1,
  )
  if !flag.is_empty() {
    let flags = [
      "", "resource", "resource_name", "panorama", "soundevent", "subclass", "entity_name",
      "localize",
    ]
    for i in 1.. ()
    BNumber(kind, bits) => {
      let width = kv3_binary_width(kind)
      if width < 8 && bits >= 1UL << (width * 8) {
        raise InvalidBinary(0, "Number payload exceeds storage width")
      }
      self.scalar_bytes += width
      valve_uint(
        match width {
          1 => self.one
          2 => self.two
          4 => self.four
          _ => self.eight
        },
        bits,
        width,
      )
    }
    BString(s) => valve_uint(self.four, self.string_id(s), 4)
    BBlob(bytes) => {
      if bytes.length() > self.options.max_bytes - self.blob_bytes {
        raise InvalidBinary(0, "KV3 blobs exceed budget")
      }
      self.blob_bytes += bytes.length()
      self.blobs.push(bytes)
    }
    BArray(values) => {
      self.arrays += 1
      valve_uint(self.four, values.length().to_uint64(), 4)
      for child in values {
        self.value(child, depth + 1, "")
      }
    }
    BObject(entries) => {
      self.object_count += 1
      valve_uint(
        if self.version >= 5 {
          self.objects
        } else {
          self.four
        },
        entries.length().to_uint64(),
        4,
      )
      let seen : Map[String, Bool] = Map([])
      for (key, child) in entries {
        if seen.contains(key) {
          raise InvalidBinary(0, "Duplicate binary KV3 object member")
        }
        seen[key] = true
        valve_uint(self.four, self.string_id(key), 4)
        self.value(child, depth + 1, "")
      }
    }
    _ => ()
  }
  if self.scalar_bytes > self.options.max_bytes {
    raise InvalidBinary(0, "KV3 output exceeds budget")
  }
}

///|
fn kv3_write_lane(output : @buffer.Buffer, lane : Bytes, width : Int32) -> Unit {
  if lane.is_empty() {
    return
  }
  while output.length() % width != 0 {
    output.write_byte(b'\x00')
  }
  output.write_bytes(lane[:])
}

///|
/// Write interoperable versions 4 or 5. Numbers preserve original storage width
/// and IEEE bits. Container layout is canonical; compressed bytes need not match.
pub fn encode_kv3_binary(
  document : KV3BinaryDocument,
  version~ : Int32 = 5,
  compression~ : Int32 = 0,
  options~ : BinaryOptions = BinaryOptions::default(),
) -> Bytes raise {
  check_binary_options(options)
  if (version != 4 && version != 5) || compression < 0 || compression > 2 {
    raise InvalidBinary(
      0, "KV3 output supports versions 4/5 and compression 0/1/2",
    )
  }
  let writer = KV3BinaryWriter::{
    one: @buffer.Buffer(),
    two: @buffer.Buffer(),
    four: @buffer.Buffer(),
    eight: @buffer.Buffer(),
    types: @buffer.Buffer(),
    objects: @buffer.Buffer(),
    blobs: [],
    names: [],
    strings: Map([]),
    version,
    options,
    nodes: 0,
    arrays: 0,
    object_count: 0,
    blob_bytes: 0,
    scalar_bytes: 0,
  }
  writer.value(document.root, 0, "")
  let strings = @buffer.Buffer()
  for name in writer.names {
    strings.write_bytes(@utf8.encode(name[:])[:])
    strings.write_byte(b'\x00')
  }
  let blob_data = @buffer.Buffer()
  let frame_sizes = @buffer.Buffer()
  for blob in writer.blobs {
    if compression == 1 {
      let mut offset = 0
      while offset < blob.length() {
        let count = (blob.length() - offset).min(16384)
        let encoded = valve_lz4_encode(blob[offset:offset + count].to_owned())
        valve_uint(frame_sizes, encoded.length().to_uint64(), 2)
        blob_data.write_bytes(encoded[:])
        offset += count
      }
    } else {
      blob_data.write_bytes(blob[:])
    }
  }
  let raw_blobs = blob_data.to_bytes()
  let compressed_blobs = if compression == 2 && !writer.blobs.is_empty() {
    valve_compress(raw_blobs, 2)
  } else {
    raw_blobs
  }
  let first = @buffer.Buffer()
  let second = @buffer.Buffer()
  let four = @buffer.Buffer()
  if version == 4 {
    valve_uint(four, writer.names.length().to_uint64(), 4)
  }
  four.write_bytes(writer.four.to_bytes()[:])
  if version == 5 {
    first.write_bytes(strings.to_bytes()[:])
    while first.length() % 4 != 0 {
      first.write_byte(b'\x00')
    }
    valve_uint(first, writer.names.length().to_uint64(), 4)
    second.write_bytes(writer.objects.to_bytes()[:])
  }
  let main = if version == 4 { first } else { second }
  kv3_write_lane(main, writer.one.to_bytes(), 1)
  kv3_write_lane(main, writer.two.to_bytes(), 2)
  kv3_write_lane(main, four.to_bytes(), 4)
  kv3_write_lane(main, writer.eight.to_bytes(), 8)
  if version == 4 {
    while main.length() % 8 != 0 {
      main.write_byte(b'\x00')
    }
    main.write_bytes(strings.to_bytes()[:])
  }
  main.write_bytes(writer.types.to_bytes()[:])
  for blob in writer.blobs {
    valve_uint(main, blob.length().to_uint64(), 4)
  }
  valve_uint(main, 0xffeedd00UL, 4)
  main.write_bytes(frame_sizes.to_bytes()[:])
  if first.length() > options.max_bytes - second.length() ||
    writer.blob_bytes > options.max_bytes - first.length() - second.length() {
    raise InvalidBinary(0, "KV3 aggregate output exceeds budget")
  }
  let packed1 = valve_compress(first.to_bytes(), compression)
  let packed2 = if version == 5 {
    valve_compress(second.to_bytes(), compression)
  } else {
    b""
  }
  let output = @buffer.Buffer()
  valve_uint(output, 0x4b563300UL + version.to_uint64(), 4)
  output.write_bytes(kv3_guid_bytes(document.format_id)[:])
  valve_uint(output, compression.to_uint64(), 4)
  valve_uint(output, 0UL, 2)
  valve_uint(output, if compression == 1 { 16384UL } else { 0UL }, 2)
  valve_uint(
    output,
    (if version == 5 { strings.length() } else { writer.one.length() }).to_uint64(),
    4,
  )
  valve_uint(
    output,
    (if version == 5 { 1 } else { four.length() / 4 }).to_uint64(),
    4,
  )
  valve_uint(
    output,
    (if version == 5 { 0 } else { writer.eight.length() / 8 }).to_uint64(),
    4,
  )
  valve_uint(
    output,
    (writer.types.length() + (if version == 4 { strings.length() } else { 0 })).to_uint64(),
    4,
  )
  valve_uint(output, writer.object_count.min(65535).to_uint64(), 2)
  valve_uint(output, writer.arrays.min(65535).to_uint64(), 2)
  valve_uint(output, (first.length() + second.length()).to_uint64(), 4)
  valve_uint(
    output,
    (packed1.length() +
    packed2.length() +
    (if compression == 2 { compressed_blobs.length() } else { 0 })).to_uint64(),
    4,
  )
  valve_uint(output, writer.blobs.length().to_uint64(), 4)
  valve_uint(output, writer.blob_bytes.to_uint64(), 4)
  valve_uint(
    output,
    (if version == 4 { writer.two.length() / 2 } else { 0 }).to_uint64(),
    4,
  )
  valve_uint(
    output,
    (if version == 5 { frame_sizes.length() } else { 0 }).to_uint64(),
    4,
  )
  if version == 5 {
    for
      n in [
        first.length(),
        if compression == 0 {
          0
        } else {
          packed1.length()
        },
        second.length(),
        if compression == 0 {
          0
        } else {
          packed2.length()
        },
        writer.one.length(),
        writer.two.length() / 2,
        four.length() / 4,
        writer.eight.length() / 8,
        writer.nodes,
        writer.object_count,
        writer.arrays,
        writer.nodes,
      ] {
      valve_uint(output, n.to_uint64(), 4)
    }
  }
  output.write_bytes(packed1[:])
  output.write_bytes(packed2[:])
  output.write_bytes(compressed_blobs[:])
  if !writer.blobs.is_empty() {
    valve_uint(output, 0xffeedd00UL, 4)
  }
  if output.length() > options.max_bytes {
    raise InvalidBinary(0, "KV3 encoded bytes exceed budget")
  }
  output.to_bytes()
}