Cbuffer

use @memcpy[Pointer[U8] ref](dest: Pointer[None] tag, src: Pointer[None] tag, size: USize)
use @memmove[Pointer[U8] ref](dest: Pointer[None] tag, src: Pointer[None] tag, size: USize)
use @pony_ctx[Pointer[None]]()
use @pony_alloc[Pointer[U8] ref](ctx: Pointer[None], size: USize)
use @explicit_bzero[None](ptr: Pointer[U8] tag, size: USize)

class CBuffer[A: Integer[A] val = ISize]
  var _ptr: Pointer[U8] ref
  var _allocated: USize = 0
  var _written: CBox[A] = CBox[A](A.from[USize](0))

  new create(size: USize, bzero: Bool = true) =>
    """
    Allocate a buffer of `size` bytes, zero-filled. CBuffer explicitly
    does not support resize.  To grow, construct a new CBuffer and copy.

    The type parameter `A` selects the numeric type backing `written_size`.
    It defaults to `ISize` so the negative-sentinel idiom remains available;
    callers wanting an unsigned width (e.g. `CBuffer[U32]`) can override.
    """
    _ptr = @pony_alloc(@pony_ctx(), size)
    _allocated = size
    if (bzero) then @explicit_bzero(_ptr, _allocated) end

  fun allocated(): USize =>
    """
    Returns allocated capacity in bytes.
    """
    _allocated

  fun ref reset() =>
    """
    Zero-fill the buffer and reset `written_size` to 0.
    """
    @explicit_bzero(_ptr, _allocated)
    _written.value = A.from[USize](0)

  fun ref ptr(): Pointer[U8] ref =>
    """
    Base pointer for FFI calls.
    """
    _ptr

  fun get_written_size(): A =>
    """
    Most recent filled length, in bytes. When `A` is signed, callers may
    use negative values as out-of-band sentinels.
    """
    _written.value

  fun ref set_written_size(n: A) =>
    """
    Set `written_size` from Pony code. Useful when a caller wants to mark
    the buffer with a sentinel without going through C.
    """
    _written.value = n

  fun ref written_size_ptr(): CBox[A] =>
    """
    Address of the `written_size` field, for C functions that report the
    filled length through an out-parameter.
    """
    _written

  fun ref write(str: String val, bzero: Bool = true): Bool =>
    """
    Copy `str` into the buffer and set `written_size` to its length. Returns
    false (leaving the buffer unchanged) if `str` exceeds capacity.

    Refuses truncation rather than silently dropping bytes.

    bzero can be disabled for performance reasons if dealing with massive
    allocations in hot-paths.  You probably shouldn't unless you're certain
    you know what you're doing.
    """
    if str.size() > _allocated then return false end
    if (bzero) then @explicit_bzero(_ptr, _allocated) end
    @memcpy(_ptr, str.cpointer(), str.size())
    _written.value = A.from[USize](str.size())
    true

  fun ref write_array(arr: Array[U8] val, bzero: Bool = true): Bool =>
    """
    Copy `arr` into the buffer and set `written_size` to its length. Returns
    false (leaving the buffer unchanged) if the buffer is not allocated or
    `arr` exceeds capacity.

    Refuses truncation rather than silently dropping bytes.

    bzero can be disabled for performance reasons if dealing with massive
    allocations in hot-paths.  You probably shouldn't unless you're certain
    you know what you're doing.
    """
    if arr.size() > _allocated then return false end
    if (bzero) then @explicit_bzero(_ptr, _allocated) end
    @memcpy(_ptr, arr.cpointer(), arr.size())
    _written.value = A.from[USize](arr.size())
    true

  fun ref copy_string_truncated(): String iso^ ? =>
    """
    Copy the filled region out as a String. Errors if the buffer is not
    allocated or `written_size` is negative. If `written_size` exceeds
    `cap()` (e.g. a C caller reporting truncation), the result is clamped
    to `cap()` rather than walking off the end of the allocation.
    """
    if _written.value < A.from[USize](0) then error end
    let size = if _written.value.usize() > _allocated then _allocated else _written.value.usize() end
    String.from_cpointer(_ptr, size, size).clone()

  fun copy_array_truncated(): Array[U8] iso^ ? =>
    """
    Copy the filled region out as an Array[U8]. Same error and clamp
    semantics as `copy_string_truncated()`.
    """
    if _written.value < A.from[USize](0) then error end
    let size = if _written.value.usize() > _allocated then _allocated else _written.value.usize() end
    let rv: Array[U8] iso = recover iso Array[U8].init(0, size) end
    @memcpy(rv.cpointer(), _ptr, size)
    consume rv

  fun ref copy_string(): String iso^ ? =>
    """
    Copy the filled region out as a String. Errors if the buffer is not
    allocated or `written_size` is negative, or if written_size is greater
    than the allocated space.
    """
    if _written.value < A.from[USize](0) then error end
    let size = _written.value.usize()
    if size > _allocated then error end
    String.from_cpointer(_ptr, size, size).clone()

  fun copy_array(): Array[U8] iso^ ? =>
    """
    Copy the filled region out as an Array[U8]. Same error semantics as
    `copy_string()`.
    """
    if _written.value < A.from[USize](0) then error end
    let size = _written.value.usize()
    if size > _allocated then error end
    let rv: Array[U8] iso = recover iso Array[U8].init(0, size) end
    @memcpy(rv.cpointer(), _ptr, size)
    consume rv