///|
/// A byte sink the serializer writes to.
///
/// Only the two raw operations are required; everything else (fixed-width
/// big-endian primitives, varints, floats) has a default implementation built
/// on them, so any custom sink only has to provide `write_u8` and
/// `write_bytes`. The built-in `ByteOutput` overrides the fixed-width methods
/// with bulk fast paths.
///
/// The wire format is fixed: all fixed-width values are big-endian, varints
/// are LEB128-style base-128 (least-significant 7 bits first, high bit is the
/// continuation bit) with zigzag mapping for the signed variant.
pub(open) trait BinaryOutput {
  /// Write a single raw byte.
  fn write_u8(Self, Byte) -> Unit raise DesertFailure
  /// Write a run of raw bytes.
  fn write_bytes(Self, BytesView) -> Unit raise DesertFailure
  /// Write a 1-byte signed integer (reinterpreted).
  fn write_i8(Self, Int32) -> Unit raise DesertFailure = _
  /// Write a 2-byte big-endian unsigned integer.
  fn write_u16(Self, UInt16) -> Unit raise DesertFailure = _
  /// Write a 2-byte big-endian signed integer.
  fn write_i16(Self, Int32) -> Unit raise DesertFailure = _
  /// Write a 4-byte big-endian unsigned integer.
  fn write_u32(Self, UInt32) -> Unit raise DesertFailure = _
  /// Write a 4-byte big-endian signed integer.
  fn write_i32(Self, Int32) -> Unit raise DesertFailure = _
  /// Write an 8-byte big-endian unsigned integer.
  fn write_u64(Self, UInt64) -> Unit raise DesertFailure = _
  /// Write an 8-byte big-endian signed integer.
  fn write_i64(Self, Int64) -> Unit raise DesertFailure = _
  /// Write a 4-byte IEEE-754 float, big-endian bit pattern.
  fn write_f32(Self, Float) -> Unit raise DesertFailure = _
  /// Write an 8-byte IEEE-754 double, big-endian bit pattern.
  fn write_f64(Self, Double) -> Unit raise DesertFailure = _
  /// Write a base-128 varint of an unsigned 32-bit value (1-5 bytes).
  fn write_var_u32(Self, UInt32) -> Unit raise DesertFailure = _
  /// Write a base-128 varint of a signed 32-bit value, zigzag-mapped
  /// (1-5 bytes, symmetric with `write_var_u32`).
  fn write_var_i32(Self, Int32) -> Unit raise DesertFailure = _
  /// Write a string as UTF-8. The default implementation transcodes into a
  /// scratch buffer and forwards to `write_bytes`; buffer-backed sinks
  /// override it with a zero-allocation pass (lone surrogates are the
  /// caller's responsibility — this operation writes the view as-is).
  fn write_string_utf8(Self, StringView) -> Unit raise DesertFailure = _
}

///|
/// Default `BinaryOutput` implementations, built on `write_u8`.
impl BinaryOutput with fn write_i8(self, value : Int32) {
  self.write_u8(value.to_byte())
}

///|
impl BinaryOutput with fn write_u16(self, value : UInt16) {
  let v = value.to_uint()
  self.write_u8((v >> 8).to_byte())
  self.write_u8(v.to_byte())
}

///|
impl BinaryOutput with fn write_i16(self, value : Int32) {
  self.write_u16((value & 0xFFFF).to_uint16())
}

///|
impl BinaryOutput with fn write_u32(self, value : UInt32) {
  self.write_u8((value >> 24).to_byte())
  self.write_u8(((value >> 16) & 0xFF).to_byte())
  self.write_u8(((value >> 8) & 0xFF).to_byte())
  self.write_u8((value & 0xFF).to_byte())
}

///|
impl BinaryOutput with fn write_i32(self, value : Int32) {
  self.write_u32(value.reinterpret_as_uint())
}

///|
impl BinaryOutput with fn write_u64(self, value : UInt64) {
  let v = value
  self.write_u8((v >> 56).to_byte())
  self.write_u8(((v >> 48) & 0xFFUL).to_byte())
  self.write_u8(((v >> 40) & 0xFFUL).to_byte())
  self.write_u8(((v >> 32) & 0xFFUL).to_byte())
  self.write_u8(((v >> 24) & 0xFFUL).to_byte())
  self.write_u8(((v >> 16) & 0xFFUL).to_byte())
  self.write_u8(((v >> 8) & 0xFFUL).to_byte())
  self.write_u8((v & 0xFFUL).to_byte())
}

///|
impl BinaryOutput with fn write_i64(self, value : Int64) {
  self.write_u64(value.reinterpret_as_uint64())
}

///|
impl BinaryOutput with fn write_f32(self, value : Float) {
  self.write_u32(value.reinterpret_as_uint())
}

///|
impl BinaryOutput with fn write_f64(self, value : Double) {
  self.write_u64(value.reinterpret_as_uint64())
}

///|
impl BinaryOutput with fn write_var_u32(self, value : UInt32) {
  let mut v = value
  while v >= 0x80 {
    self.write_u8(((v & 0x7F) | 0x80).to_byte())
    v = v >> 7
  }
  self.write_u8(v.to_byte())
}

///|
/// Zigzag-mapped signed varint: `(v << 1) ^ (v >> 31)`, matching the Scala
/// `writeVarInt(value, optimizeForPositive = false)` and the Rust
/// `write_var_i32`.
impl BinaryOutput with fn write_var_i32(self, value : Int32) {
  let zigzag = (value << 1) ^ (value >> 31)
  self.write_var_u32(zigzag.reinterpret_as_uint())
}

///|
/// A `BinaryOutput` that accumulates into an in-memory growable byte buffer.
///
/// Overrides the fixed-width operations with the bulk fast paths of the core
/// `Buffer`.
pub struct ByteOutput {
  priv buffer : Buffer
}

///|
/// Create an empty output. Pass `capacity~` to pre-allocate.
pub fn ByteOutput::new(capacity~ : Int32 = 128) -> ByteOutput {
  { buffer: Buffer(size_hint=capacity), }
}

///|
pub impl BinaryOutput for ByteOutput with fn write_u8(self, byte) {
  self.buffer.write_byte(byte)
}

///|
pub impl BinaryOutput for ByteOutput with fn write_bytes(self, bytes) {
  self.buffer.write_bytesview(bytes)
}

///|
pub impl BinaryOutput for ByteOutput with fn write_i16(self, value) {
  self.buffer.write_uint16_be((value & 0xFFFF).to_uint16())
}

///|
pub impl BinaryOutput for ByteOutput with fn write_u16(self, value) {
  self.buffer.write_uint16_be(value)
}

///|
pub impl BinaryOutput for ByteOutput with fn write_i32(self, value) {
  self.buffer.write_int_be(value)
}

///|
pub impl BinaryOutput for ByteOutput with fn write_u32(self, value) {
  self.buffer.write_uint_be(value)
}

///|
pub impl BinaryOutput for ByteOutput with fn write_i64(self, value) {
  self.buffer.write_int64_be(value)
}

///|
pub impl BinaryOutput for ByteOutput with fn write_u64(self, value) {
  self.buffer.write_uint64_be(value)
}

///|
pub impl BinaryOutput for ByteOutput with fn write_f32(self, value) {
  self.buffer.write_float_be(value)
}

///|
pub impl BinaryOutput for ByteOutput with fn write_f64(self, value) {
  self.buffer.write_double_be(value)
}

///|
/// Zero-allocation pass-through to the core Buffer's UTF-8 writer.
pub impl BinaryOutput for ByteOutput with fn write_string_utf8(self, s) {
  self.buffer.write_string_utf8(s)
}

///|
/// The bytes written so far, as an immutable snapshot.
pub fn ByteOutput::to_bytes(self : ByteOutput) -> Bytes {
  self.buffer.to_bytes()
}

///|
/// Number of bytes written so far.
pub fn ByteOutput::length(self : ByteOutput) -> Int32 {
  self.buffer.length()
}

///|
/// Promote the `BinaryOutput` operations to regular methods on `ByteOutput`.
pub extend ByteOutput with BinaryOutput::{
  write_u8,
  write_bytes,
  write_i8,
  write_u16,
  write_i16,
  write_u32,
  write_i32,
  write_u64,
  write_i64,
  write_f32,
  write_f64,
  write_var_u32,
  write_var_i32,
  write_string_utf8,
}

///|
/// Discard the bytes written so far, reusing the allocated capacity.
pub fn ByteOutput::reset(self : ByteOutput) -> Unit {
  self.buffer.reset()
}

///|
/// Default `write_string_utf8`: transcode into a scratch array (exact-size
/// allocation), then bulk-write. Lone surrogates are substituted with `?`.
impl BinaryOutput with fn write_string_utf8(self, s : StringView) {
  let owned = s.to_owned()
  let byte_len = utf8_byte_length(owned)
  if byte_len == 0 {
    return
  }
  let scratch : FixedArray[Byte] = FixedArray::make(byte_len, b'\x00')
  let _ = write_utf8(owned, scratch, 0)
  self.write_bytes(Bytes::from_array(scratch).exact_view(end=byte_len))
}