///|
/// WebAssembly table with target-specific backing storage
struct Table {
  mut reservation : @jit_ffi.Reservation?
  mut binding : ResourceBinding?
  mut closed : Bool
  elem_type : @types.ValueType
  mut storage : TableStorage
  max : Int64?
  is_table64 : Bool // true for table64 (i64-indexed table)
} derive(Debug)

///|
pub fn Table::Table(
  elem_type : @types.ValueType,
  min : Int32,
  max : Int32?,
  init_value~ : @types.Value = Null,
  is_table64~ : Bool = false,
  resource_budget~ : ResourceBudget? = None,
) -> Table raise RuntimeError {
  Table::allocate(
    elem_type,
    min.to_int64() & 0xFFFF_FFFFL,
    max.map(fn(value) { value.to_int64() & 0xFFFF_FFFFL }),
    init_value,
    is_table64,
    resource_budget,
  )
}

///|
fn Table::allocate(
  elem_type : @types.ValueType,
  min : Int64,
  max : Int64?,
  init_value : @types.Value,
  is_table64 : Bool,
  resource_budget : ResourceBudget?,
) -> Table raise RuntimeError {
  if max is Some(maximum) &&
    maximum.reinterpret_as_uint64() < min.reinterpret_as_uint64() {
    raise InvalidTableType("maximum is below minimum")
  }
  // A native reference occupies one pointer-sized slot. Check multiplication
  // before narrowing to the backing MoonBit array's supported element count.
  if min.reinterpret_as_uint64() > 0x1FFF_FFFF_FFFF_FFFFUL {
    raise TableAllocationOverflow
  }
  if min > 0x7FFF_FFFFL {
    raise TableAllocationFailed
  }
  guard (match elem_type {
    I32 | I64 | F32 | F64 | V128 => false
    _ => true
  }) else {
    raise InvalidTableType("element type must be a reference type")
  }
  if min != 0L && !value_fits_kind(init_value, elem_type) {
    raise TypeMismatch
  }
  let reservation = resource_budget.map(budget => {
    budget.reservation(TableElements, min)
  })
  let size = min.to_int()
  let elements : Array[@types.Value] = []
  for _ in 0.. Table raise RuntimeError {
  Table::allocate(
    ty.elem_type,
    ty.limits.min,
    ty.limits.max,
    init_value,
    ty.is_table64,
    resource_budget,
  )
}

///|
pub fn Table::get(self : Table, idx : Int32) -> @types.Value raise RuntimeError {
  self.get_at(idx.to_int64() & 0xFFFF_FFFFL)
}

///|
fn Table::ensure_open(self : Table) -> Unit raise RuntimeError {
  check_resource_open(self.binding)
  guard !self.closed else { raise InvalidHandle }
}

///|
fn Table::check_range(
  self : Table,
  offset : Int64,
  count : Int64,
) -> Unit raise RuntimeError {
  self.ensure_open()
  let size = self.size().to_int64()
  if offset < 0L || count < 0L || count > size || offset > size - count {
    raise OutOfBoundsTableAccess
  }
}

///|
pub fn Table::get_at(
  self : Table,
  idx : Int64,
) -> @types.Value raise RuntimeError {
  self.check_range(idx, 1L)
  self.read_element(idx.to_int())
}

///|
pub fn Table::set(
  self : Table,
  idx : Int32,
  value : @types.Value,
) -> Unit raise RuntimeError {
  self.set_at(idx.to_int64() & 0xFFFF_FFFFL, value)
}

///|
pub fn Table::set_at(
  self : Table,
  idx : Int64,
  value : @types.Value,
) -> Unit raise RuntimeError {
  self.check_range(idx, 1L)
  guard value_fits_kind(value, self.elem_type) else { raise TypeMismatch }
  self.write_element(idx.to_int(), value)
}

///|
pub fn Table::size(self : Table) -> Int32 {
  self.storage_size()
}

///|
pub fn Table::grow(self : Table, delta : Int32, init : @types.Value) -> Int32 {
  let result = self.grow64(delta.to_int64() & 0xFFFF_FFFFL, init) catch {
    _ => -1L
  }
  result.to_int()
}

///|
/// Grow using an unsigned table64 count, preserving allocation failures.
pub fn Table::grow64(
  self : Table,
  delta : Int64,
  init : @types.Value,
) -> Int64 raise RuntimeError {
  self.ensure_open()
  guard value_fits_kind(init, self.elem_type) else { raise TypeMismatch }
  let old_size = self.size().to_uint64()
  if delta == 0L {
    return old_size.reinterpret_as_int64()
  }
  let new_size = old_size + delta.reinterpret_as_uint64()
  if new_size < old_size {
    return -1L
  }
  if self.max is Some(max) && new_size > max.reinterpret_as_uint64() {
    return -1L
  }
  if new_size > 0x1FFF_FFFF_FFFF_FFFFUL {
    raise TableAllocationFailed
  }
  if new_size > 0x7FFF_FFFFUL {
    return -1L
  }
  self.grow_storage(
    delta,
    init,
    old_size.reinterpret_as_int64(),
    new_size.reinterpret_as_int64(),
  )
}

///|
/// Fill a range of table elements with a value
/// table.fill: fill table[d..d+n] with value
pub fn Table::fill(
  self : Table,
  dest : Int32,
  value : @types.Value,
  n : Int32,
) -> Unit raise RuntimeError {
  self.fill_at(
    dest.to_int64() & 0xFFFF_FFFFL,
    value,
    n.to_int64() & 0xFFFF_FFFFL,
  )
}

///|
pub fn Table::fill_at(
  self : Table,
  dest : Int64,
  value : @types.Value,
  n : Int64,
) -> Unit raise RuntimeError {
  guard value_fits_kind(value, self.elem_type) else { raise TypeMismatch }
  self.check_range(dest, n)
  self.fill_elements(dest.to_int(), n.to_int(), value)
}

///|
pub fn Table::copy_from(
  self : Table,
  dest : Int32,
  src_table : Table,
  src : Int32,
  n : Int32,
) -> Unit raise RuntimeError {
  self.copy_from_at(
    dest.to_int64() & 0xFFFF_FFFFL,
    src_table,
    src.to_int64() & 0xFFFF_FFFFL,
    n.to_int64() & 0xFFFF_FFFFL,
  )
}

///|
pub fn Table::copy_from_at(
  self : Table,
  dest : Int64,
  src_table : Table,
  src : Int64,
  n : Int64,
) -> Unit raise RuntimeError {
  check_resource_copy(self.binding, src_table.binding)
  self.check_range(dest, n)
  src_table.check_range(src, n)
  self.copy_elements(dest.to_int(), src_table, src.to_int(), n.to_int())
}

///|
/// Get element type for type checking
pub fn Table::get_elem_type(self : Table) -> @types.ValueType {
  self.elem_type
}

///|
/// Get table limits (min, max) for type checking
pub fn Table::get_limits(self : Table) -> (Int32, Int32?) {
  (
    self.size(),
    match self.max {
      Some(max) if max.reinterpret_as_uint64() <= 0xFFFF_FFFFUL =>
        Some(max.to_int())
      _ => None
    },
  )
}

///|
/// Check if this is a table64 (i64-indexed table)
pub fn Table::is_table64(self : Table) -> Bool {
  self.is_table64
}

///|
/// Full declared limits for import compatibility and native growth checks.
pub fn Table::get_limits64(self : Table) -> (Int64, Int64?) {
  (self.size().to_int64(), self.max)
}

///|
pub fn Table::close(self : Table) -> Unit {
  self.closed = true
  if self.binding is Some(binding) {
    binding.active.val = false
  }
  self.close_storage()
  if self.reservation is Some(reservation) {
    reservation.close()
  }
  self.reservation = None
}

///|
#cfg(target="native")
priv enum TableStorage {
  Managed(Array[@types.Value])
  Native(NativeTableStorage)
} derive(Debug)

///|
#cfg(target="native")
priv struct NativeTableStorage {
  table : @native_bridge.JITTable
  references : StoreReferences
}

///|
#cfg(target="native")
impl Debug for NativeTableStorage with fn to_repr(_self) {
  Repr::literal("NativeTableStorage")
}

///|
#cfg(target="native")
fn Table::storage_size(self : Table) -> Int32 {
  match self.storage {
    Managed(elements) => elements.length()
    Native(storage) => storage.table.get_size()
  }
}

///|
#cfg(target="native")
fn Table::read_element(
  self : Table,
  index : Int32,
) -> @types.Value raise RuntimeError {
  match self.storage {
    Managed(elements) => elements[index]
    Native(storage) => {
      let native = storage.table.raw_ptr()
      let external = self.elem_type is ExternRef ||
        self.elem_type is RefExtern ||
        self.elem_type is NullExternRef
      let metadata = if external { native.word(index, 1).to_int() } else { -1 }
      let word = native.word(index, 0)
      let value = storage.references.decode_table_value(
        self.elem_type,
        metadata,
        word,
        false,
      )
      // Cache a wrapper created at this native-to-host boundary in the entry,
      // so later reads preserve identity without allocating another handle.
      if external && metadata < 0 && value is ExternRef(handle) && handle < 0 {
        storage.table.set(index, word, -1 - handle)
      }
      value
    }
  }
}

///|
#cfg(target="native")
fn Table::root_at(
  self : Table,
  index : Int32,
) -> @types.Value raise RuntimeError {
  match self.storage {
    Managed(elements) => elements[index]
    Native(storage) =>
      storage.references.decode_table_value(
        self.elem_type,
        storage.table.raw_ptr().word(index, 1).to_int(),
        storage.table.raw_ptr().word(index, 0),
        true,
      )
  }
}

///|
#cfg(target="native")
fn Table::write_element(
  self : Table,
  index : Int32,
  value : @types.Value,
) -> Unit raise RuntimeError {
  match self.storage {
    Managed(elements) => elements[index] = value
    Native(storage) => {
      let (type_index, word) = storage.references.encode_table_value(value)
      storage.table.set(index, word, type_index)
    }
  }
}

///|
#cfg(target="native")
fn Table::fill_elements(
  self : Table,
  offset : Int32,
  length : Int32,
  value : @types.Value,
) -> Unit raise RuntimeError {
  if length == 0 {
    return
  }
  match self.storage {
    Managed(elements) =>
      for i in offset..<(offset + length) {
        elements[i] = value
      }
    Native(storage) => {
      let (metadata, word) = storage.references.encode_table_value(value)
      guard storage.table.raw_ptr().fill(offset, length, word, metadata) else {
        raise InvalidHandle
      }
    }
  }
}

///|
#cfg(target="native")
fn Table::copy_elements(
  self : Table,
  destination : Int32,
  source : Table,
  offset : Int32,
  length : Int32,
) -> Unit raise RuntimeError {
  if self.elem_type == source.elem_type &&
    self.storage is Native(target) &&
    source.storage is Native(origin) {
    guard target.table
      .raw_ptr()
      .copy_from(destination, origin.table.raw_ptr(), offset, length) else {
      raise InvalidHandle
    }
    return
  }
  if destination <= offset {
    for i in 0..= 0; i = i - 1 {
      self.write_element(destination + i, source.read_element(offset + i))
    }
  }
}

///|
#cfg(target="native")
fn Table::grow_storage(
  self : Table,
  delta : Int64,
  init : @types.Value,
  old_size : Int64,
  new_size : Int64,
) -> Int64 raise RuntimeError {
  match self.storage {
    Managed(elements) => {
      if self.reservation is Some(reservation) && !reservation.resize(new_size) {
        return -1L
      }
      for _ in 0.. {
      if self.reservation is Some(reservation) && !reservation.resize(new_size) {
        return -1L
      }
      let created : Array[Int32] = []
      let mut committed = false
      defer (if !committed {
        for index in created {
          storage.references.release_native_exception(index)
        }
        if self.reservation is Some(reservation) {
          reservation.resize(old_size) |> ignore
        }
      })
      let (type_index, value) = storage.references.encode_table_value(
        init,
        created_exceptions=created,
      )
      let result = storage.table.raw_ptr().grow_value(delta, value, type_index)
      committed = result >= 0L
      return result
    }
  }
  old_size
}

///|
#cfg(target="native")
fn Table::close_storage(self : Table) -> Unit {
  if self.storage is Native(storage) {
    storage.table.raw_ptr().invalidate()
  }
  self.storage = Managed([])
}