///|
/// Result produced by a MoonBit callback that implements a Wren foreign method.
pub(all) enum ForeignCallResult {
  ForeignCallSucceeded(Value)
  ForeignCallFailed(String)
}

///|
/// Runtime-independent value tree returned by a context-aware foreign method.
/// Lists and maps are materialized inside the receiving VM after the callback
/// returns, so host packages never need access to runtime object identifiers.
pub(all) enum ForeignOwnedValue {
  ForeignNull
  ForeignBool(Bool)
  ForeignNumber(Double)
  ForeignString(String)
  ForeignBytes(Array[Int])
  ForeignRange(Double, Double, Bool)
  ForeignList(Array[ForeignOwnedValue])
  ForeignMap(Array[ForeignOwnedMapEntry])
  ForeignExisting(Value)
}

///|
pub(all) struct ForeignOwnedMapEntry {
  key : ForeignOwnedValue
  value : ForeignOwnedValue
}

///|
pub fn ForeignOwnedMapEntry::new(
  key : ForeignOwnedValue,
  value : ForeignOwnedValue,
) -> ForeignOwnedMapEntry {
  { key, value, }
}

///|
/// Arguments visible to a context-aware foreign method. The context is valid
/// only for the duration of its callback and cannot re-enter the VM.
pub struct ForeignCallContext {
  receiver : Value
  arguments : Array[Value]
}

///|
pub fn ForeignCallContext::receiver(self : ForeignCallContext) -> Value {
  self.receiver
}

///|
pub fn ForeignCallContext::argument_count(self : ForeignCallContext) -> Int {
  self.arguments.length()
}

///|
pub fn ForeignCallContext::argument(
  self : ForeignCallContext,
  index : Int,
) -> Value? {
  if index < 0 || index >= self.arguments.length() {
    None
  } else {
    Some(self.arguments[index])
  }
}

///|
pub fn ForeignCallContext::arguments(self : ForeignCallContext) -> Array[Value] {
  self.arguments.copy()
}

///|
pub(all) enum ForeignContextResult {
  ForeignContextReturned(ForeignOwnedValue)
  ForeignContextAborted(String)
}

///|
/// Result returned while allocating the host-owned state of a foreign object.
/// The integer is an opaque handle chosen and managed by the embedding host.
pub(all) enum ForeignAllocationResult {
  ForeignAllocationSucceeded(Int)
  ForeignAllocationFailed(String)
}

///|
/// Host lifecycle callbacks associated with one Wren `foreign class`.
pub struct ForeignClassBinding {
  module_name : String
  class_name : String
  allocate : (Array[Value]) -> ForeignAllocationResult
  finalize : (Int) -> Unit
}

///|
pub fn ForeignClassBinding::new(
  module_name : String,
  class_name : String,
  allocate : (Array[Value]) -> ForeignAllocationResult,
  finalize : (Int) -> Unit,
) -> ForeignClassBinding {
  { module_name, class_name, allocate, finalize, }
}

///|
fn ForeignClassBinding::matches(
  self : ForeignClassBinding,
  module_name : String,
  class_name : String,
) -> Bool {
  self.module_name == module_name && self.class_name == class_name
}

///|
fn ForeignClassBinding::allocate(
  self : ForeignClassBinding,
  arguments : Array[Value],
) -> ForeignAllocationResult {
  (self.allocate)(arguments)
}

///|
fn ForeignClassBinding::finalize(
  self : ForeignClassBinding,
  handle : Int,
) -> Unit {
  (self.finalize)(handle)
}

///|
/// One host implementation identified using Wren's binding tuple.
pub struct ForeignMethodBinding {
  module_name : String
  class_name : String
  is_static : Bool
  signature : String
  callback : ((Value, Array[Value]) -> ForeignCallResult)?
  handle_callback : ((Int, Array[Value]) -> ForeignCallResult)?
  context_callback : ((ForeignCallContext) -> ForeignContextResult)?
  handle_context_callback : ((Int, ForeignCallContext) -> ForeignContextResult)?
}

///|
pub fn ForeignMethodBinding::new(
  module_name : String,
  class_name : String,
  is_static : Bool,
  signature : String,
  callback : (Value, Array[Value]) -> ForeignCallResult,
) -> ForeignMethodBinding {
  {
    module_name,
    class_name,
    is_static,
    signature,
    callback: Some(callback),
    handle_callback: None,
    context_callback: None,
    handle_context_callback: None,
  }
}

///|
/// Bind an instance method that receives the opaque handle allocated for a
/// foreign class instead of the Wren receiver value.
pub fn ForeignMethodBinding::new_for_foreign_class(
  module_name : String,
  class_name : String,
  signature : String,
  callback : (Int, Array[Value]) -> ForeignCallResult,
) -> ForeignMethodBinding {
  {
    module_name,
    class_name,
    is_static: false,
    signature,
    callback: None,
    handle_callback: Some(callback),
    context_callback: None,
    handle_context_callback: None,
  }
}

///|
/// Bind a method through the richer VM-independent context API. The callback
/// may return nested Lists and Maps, arbitrary String bytes, or pass through an
/// existing argument value without depending on private runtime identifiers.
pub fn ForeignMethodBinding::new_context(
  module_name : String,
  class_name : String,
  is_static : Bool,
  signature : String,
  callback : (ForeignCallContext) -> ForeignContextResult,
) -> ForeignMethodBinding {
  {
    module_name,
    class_name,
    is_static,
    signature,
    callback: None,
    handle_callback: None,
    context_callback: Some(callback),
    handle_context_callback: None,
  }
}

///|
/// Context-aware variant for an instance of a `foreign class`.
pub fn ForeignMethodBinding::new_context_for_foreign_class(
  module_name : String,
  class_name : String,
  signature : String,
  callback : (Int, ForeignCallContext) -> ForeignContextResult,
) -> ForeignMethodBinding {
  {
    module_name,
    class_name,
    is_static: false,
    signature,
    callback: None,
    handle_callback: None,
    context_callback: None,
    handle_context_callback: Some(callback),
  }
}

///|
fn ForeignMethodBinding::matches(
  self : ForeignMethodBinding,
  module_name : String,
  class_name : String,
  is_static : Bool,
  signature : String,
) -> Bool {
  self.module_name == module_name &&
  self.class_name == class_name &&
  self.is_static == is_static &&
  self.signature == signature
}

///|
fn ForeignMethodBinding::call(
  self : ForeignMethodBinding,
  receiver : Value,
  arguments : Array[Value],
) -> ForeignCallResult {
  match self.callback {
    Some(callback) => callback(receiver, arguments)
    None => ForeignCallFailed("binding requires a foreign class receiver")
  }
}

///|
fn ForeignMethodBinding::call_handle(
  self : ForeignMethodBinding,
  handle : Int,
  arguments : Array[Value],
) -> ForeignCallResult {
  match self.handle_callback {
    Some(callback) => callback(handle, arguments)
    None => ForeignCallFailed("binding does not accept a foreign class handle")
  }
}

///|
fn ForeignMethodBinding::accepts_handle(self : ForeignMethodBinding) -> Bool {
  self.handle_callback is Some(_) || self.handle_context_callback is Some(_)
}

///|
fn ForeignMethodBinding::accepts_context(self : ForeignMethodBinding) -> Bool {
  self.context_callback is Some(_) || self.handle_context_callback is Some(_)
}

///|
fn ForeignMethodBinding::call_context(
  self : ForeignMethodBinding,
  receiver : Value,
  arguments : Array[Value],
) -> ForeignContextResult {
  match self.context_callback {
    Some(callback) => callback({ receiver, arguments, })
    None => ForeignContextAborted("binding requires a foreign class receiver")
  }
}

///|
fn ForeignMethodBinding::call_handle_context(
  self : ForeignMethodBinding,
  handle : Int,
  receiver : Value,
  arguments : Array[Value],
) -> ForeignContextResult {
  match self.handle_context_callback {
    Some(callback) => callback(handle, { receiver, arguments, })
    None =>
      ForeignContextAborted("binding does not accept a foreign class handle")
  }
}