///|
/// Invalid native metadata must be rejected before publishing any pointers.
pub suberror GCMetadataError {
  InvalidTypeStride
  PayloadCountMismatch
  PayloadTooLarge
} derive(Debug)

///|
pub extend GCMetadataError with Debug::{to_repr}

///|
pub extend GCMetadataError with Show::{output, to_string}

///|
pub impl Show for GCMetadataError with fn output(self, logger) {
  logger.write_string(@types.compact_show_repr(Repr(self).to_string()))
}

///|
extern "c" fn safepoint_payload_fits(count : Int32) -> Bool = "wasmoon_jit_safepoint_payload_fits"

///|
/// Check the native descriptor's representable size before allocating arrays.
pub fn PreparedSafepoints::check_count(
  count : Int32,
) -> Unit raise GCMetadataError {
  guard safepoint_payload_fits(count) else { raise PayloadTooLarge }
}

///|
#borrow(context, types, canonical, functions)
extern "c" fn bind_gc_metadata(
  context : JITContext,
  types : FixedArray[Int32],
  canonical : FixedArray[Int32],
  functions : FixedArray[Int32],
) -> Unit = "wasmoon_jit_bind_gc_metadata"

///|
/// Retain all three GC metadata arrays, replacing even empty mappings.
/// Types use six Int slots per entry, matching the native GC type-cache ABI.
/// Bind outside guest execution and do not mutate arrays while they are bound.
pub fn JITContext::bind_gc_metadata(
  self : JITContext,
  types : FixedArray[Int32],
  canonical : FixedArray[Int32],
  functions : FixedArray[Int32],
) -> Unit raise GCMetadataError {
  guard types.length() % 6 == 0 else { raise InvalidTypeStride }
  bind_gc_metadata(self, types, canonical, functions)
}

///|
/// Retain the payloads in a stable native descriptor. Bind outside execution;
/// callers must not mutate offsets while they are bound.
#borrow(self, blob, offsets)
pub extern "c" fn JITContext::bind_gc_safepoints(
  self : JITContext,
  function : Int32,
  blob : Bytes,
  offsets : FixedArray[Int32],
) -> Bool = "wasmoon_jit_bind_gc_safepoints"

///|
/// Retain a function-address snapshot. The snapshot does not own executable code.
/// Bind outside execution and do not mutate the array while it is bound.
#borrow(self, addresses)
pub extern "c" fn JITContext::bind_gc_function_addresses(
  self : JITContext,
  addresses : FixedArray[Int64],
) -> Unit = "wasmoon_jit_bind_gc_function_addresses"

///|
extern "c" fn snapshot_function_addresses(
  table : Int64,
  count : Int32,
) -> FixedArray[Int64] = "wasmoon_jit_snapshot_gc_function_addresses"

///|
/// Copy a legacy native pointer table into a typed snapshot and bind it.
/// The source must contain count readable native pointers.
pub fn JITContext::snapshot_gc_function_addresses(
  self : JITContext,
  table : Int64,
  count : Int32,
) -> Unit {
  self.bind_gc_function_addresses(snapshot_function_addresses(table, count))
}

///|
/// Immutable native views retaining their MoonBit payloads through a finalizer.
pub type PreparedSafepoints

///|
#borrow(blobs, offsets)
extern "c" fn prepare_safepoints(
  blobs : FixedArray[Bytes],
  offsets : FixedArray[FixedArray[Int32]],
) -> PreparedSafepoints = "wasmoon_jit_prepare_safepoints"

///|
/// Bind outside execution. The context retains the owner independently of callers.
#borrow(self, prepared)
pub extern "c" fn JITContext::bind_prepared_safepoints(
  self : JITContext,
  prepared : PreparedSafepoints,
) -> Bool = "wasmoon_jit_bind_prepared_safepoints"

///|
pub fn PreparedSafepoints::new(
  blobs : FixedArray[Bytes],
  offsets : FixedArray[FixedArray[Int32]],
) -> PreparedSafepoints raise GCMetadataError {
  guard blobs.length() == offsets.length() else { raise PayloadCountMismatch }
  PreparedSafepoints::check_count(blobs.length())
  let snapshot : FixedArray[FixedArray[Int32]] = FixedArray::makei(
    offsets.length(),
    i => {
      let sites = offsets[i]
      if sites.is_empty() {
        []
      } else {
        FixedArray::makei(sites.length(), j => sites[j])
      }
    },
  )
  prepare_safepoints(blobs, snapshot)
}