///|
/// Typed result returned by host-side inspection and mutation helpers.
pub(all) enum VmHostResult[T] {
  VmHostSucceeded(T)
  VmHostFailed(Diagnostic)
}

///|
pub(all) struct VmMapEntry {
  key : Value
  value : Value
}

///|
/// Cached receiver, method name, and arity for repeated host calls.
pub struct VmMethodHandle {
  vm_identity : Int
  generation : Int
  receiver : VmHandle
  method_name : String
  arity : Int
}

///|
pub(all) enum VmMethodHandleResult {
  VmMethodHandleCreated(VmMethodHandle)
  VmMethodHandleFailed(Diagnostic)
}

///|
fn host_access_error(message : String) -> Diagnostic {
  Diagnostic::new(Runtime, InvalidOperand, message, SourceSpan::empty()).with_source(
    "",
  )
}

///|
pub fn Vm::has_module(self : Vm, module_name : String) -> Bool {
  !self.closed && find_runtime_module(self.runtime(), module_name) is Some(_)
}

///|
pub fn Vm::has_variable(
  self : Vm,
  module_name : String,
  variable : String,
) -> Bool {
  if self.closed {
    return false
  }
  match find_runtime_module(self.runtime(), module_name) {
    None => false
    Some(index) =>
      self.runtime().modules[index].environment.get(variable) is Some(_)
  }
}

///|
pub fn Vm::bind_method(
  self : Vm,
  receiver : VmHandle,
  method_name : String,
  arity : Int,
) -> VmMethodHandleResult {
  if self.closed {
    return VmMethodHandleFailed(host_access_error("VM is closed"))
  }
  if method_name.length() == 0 || arity < 0 {
    return VmMethodHandleFailed(
      host_access_error("method name must be non-empty and arity non-negative"),
    )
  }
  match self.resolve_handle(receiver) {
    None =>
      VmMethodHandleFailed(host_access_error("receiver handle is invalid"))
    Some(_) =>
      VmMethodHandleCreated({
        vm_identity: self.identity,
        generation: self.generation,
        receiver,
        method_name,
        arity,
      })
  }
}

///|
pub fn Vm::call_bound(
  self : Vm,
  bound : VmMethodHandle,
  arguments : Array[Value],
) -> ProgramRunResult {
  if bound.vm_identity != self.identity || bound.generation != self.generation {
    return ProgramFailed(
      [host_access_error("method handle belongs to another VM generation")],
      [],
    )
  }
  if arguments.length() != bound.arity {
    return ProgramFailed(
      [
        Diagnostic::new(
          Runtime,
          InvalidArity,
          "bound method expects \{bound.arity} arguments, received \{arguments.length()}",
          SourceSpan::empty(),
        ).with_source(""),
      ],
      [],
    )
  }
  self.call_method(bound.receiver, bound.method_name, arguments)
}

///|
fn Vm::resolved_collection(self : Vm, handle : VmHandle) -> VmHostResult[Value] {
  if self.closed {
    return VmHostFailed(host_access_error("VM is closed"))
  }
  match self.resolve_handle(handle) {
    Some(value) => VmHostSucceeded(value)
    None => VmHostFailed(host_access_error("collection handle is invalid"))
  }
}

///|
pub fn Vm::list_snapshot(
  self : Vm,
  handle : VmHandle,
) -> VmHostResult[Array[Value]] {
  match self.resolved_collection(handle) {
    VmHostFailed(issue) => VmHostFailed(issue)
    VmHostSucceeded(ListValue(identifier)) => {
      let context = self.runtime()
      if identifier < 0 ||
        identifier >= context.lists.length() ||
        !context.lists[identifier].alive {
        return VmHostFailed(host_access_error("list has been collected"))
      }
      let copied : Array[Value] = []
      for value in context.lists[identifier].items {
        copied.push(value)
      }
      VmHostSucceeded(copied)
    }
    VmHostSucceeded(value) =>
      VmHostFailed(
        host_access_error("expected List, received \{value.type_name()}"),
      )
  }
}

///|
pub fn Vm::list_set(
  self : Vm,
  handle : VmHandle,
  index : Int,
  value : Value,
) -> VmHostResult[Unit] {
  match self.resolved_collection(handle) {
    VmHostFailed(issue) => VmHostFailed(issue)
    VmHostSucceeded(ListValue(identifier)) => {
      let context = self.runtime()
      if identifier < 0 ||
        identifier >= context.lists.length() ||
        !context.lists[identifier].alive {
        return VmHostFailed(host_access_error("list has been collected"))
      }
      let items = context.lists[identifier].items
      let normalized = if index < 0 { items.length() + index } else { index }
      if normalized < 0 || normalized >= items.length() {
        VmHostFailed(host_access_error("list index is out of bounds"))
      } else {
        items[normalized] = value
        VmHostSucceeded(())
      }
    }
    VmHostSucceeded(value) =>
      VmHostFailed(
        host_access_error("expected List, received \{value.type_name()}"),
      )
  }
}

///|
pub fn Vm::list_add(
  self : Vm,
  handle : VmHandle,
  value : Value,
) -> VmHostResult[Unit] {
  match self.resolved_collection(handle) {
    VmHostFailed(issue) => VmHostFailed(issue)
    VmHostSucceeded(ListValue(identifier)) => {
      let context = self.runtime()
      if identifier < 0 ||
        identifier >= context.lists.length() ||
        !context.lists[identifier].alive {
        return VmHostFailed(host_access_error("list has been collected"))
      }
      context.lists[identifier].items.push(value)
      VmHostSucceeded(())
    }
    VmHostSucceeded(value) =>
      VmHostFailed(
        host_access_error("expected List, received \{value.type_name()}"),
      )
  }
}

///|
pub fn Vm::map_snapshot(
  self : Vm,
  handle : VmHandle,
) -> VmHostResult[Array[VmMapEntry]] {
  match self.resolved_collection(handle) {
    VmHostFailed(issue) => VmHostFailed(issue)
    VmHostSucceeded(MapValue(identifier)) => {
      let context = self.runtime()
      if identifier < 0 ||
        identifier >= context.maps.length() ||
        !context.maps[identifier].alive {
        return VmHostFailed(host_access_error("map has been collected"))
      }
      let copied : Array[VmMapEntry] = []
      for entry in context.maps[identifier].entries {
        copied.push({ key: entry.key, value: entry.value, })
      }
      VmHostSucceeded(copied)
    }
    VmHostSucceeded(value) =>
      VmHostFailed(
        host_access_error("expected Map, received \{value.type_name()}"),
      )
  }
}

///|
pub fn Vm::map_get(
  self : Vm,
  handle : VmHandle,
  key : Value,
) -> VmHostResult[Value?] {
  match self.resolved_collection(handle) {
    VmHostFailed(issue) => VmHostFailed(issue)
    VmHostSucceeded(MapValue(identifier)) => {
      let context = self.runtime()
      if identifier < 0 ||
        identifier >= context.maps.length() ||
        !context.maps[identifier].alive {
        return VmHostFailed(host_access_error("map has been collected"))
      }
      let entries = context.maps[identifier].entries
      match find_map_key(entries, key) {
        Some(index) => VmHostSucceeded(Some(entries[index].value))
        None => VmHostSucceeded(None)
      }
    }
    VmHostSucceeded(value) =>
      VmHostFailed(
        host_access_error("expected Map, received \{value.type_name()}"),
      )
  }
}

///|
pub fn Vm::map_set(
  self : Vm,
  handle : VmHandle,
  key : Value,
  value : Value,
) -> VmHostResult[Unit] {
  if !is_valid_map_key(key) {
    return VmHostFailed(host_access_error("map key must be immutable"))
  }
  match self.resolved_collection(handle) {
    VmHostFailed(issue) => VmHostFailed(issue)
    VmHostSucceeded(MapValue(identifier)) => {
      let context = self.runtime()
      if identifier < 0 ||
        identifier >= context.maps.length() ||
        !context.maps[identifier].alive {
        return VmHostFailed(host_access_error("map has been collected"))
      }
      ignore(context.set_map_entry(identifier, key, value))
      VmHostSucceeded(())
    }
    VmHostSucceeded(other) =>
      VmHostFailed(
        host_access_error("expected Map, received \{other.type_name()}"),
      )
  }
}

///|
pub fn Vm::map_remove(
  self : Vm,
  handle : VmHandle,
  key : Value,
) -> VmHostResult[Value?] {
  match self.resolved_collection(handle) {
    VmHostFailed(issue) => VmHostFailed(issue)
    VmHostSucceeded(MapValue(identifier)) => {
      let context = self.runtime()
      if identifier < 0 ||
        identifier >= context.maps.length() ||
        !context.maps[identifier].alive {
        return VmHostFailed(host_access_error("map has been collected"))
      }
      let entries = context.maps[identifier].entries
      match find_map_key(entries, key) {
        Some(index) => VmHostSucceeded(Some(entries.remove(index).value))
        None => VmHostSucceeded(None)
      }
    }
    VmHostSucceeded(value) =>
      VmHostFailed(
        host_access_error("expected Map, received \{value.type_name()}"),
      )
  }
}

///|
/// Deliver captured output and diagnostics through host callbacks while
/// retaining the ordinary result for further inspection.
pub fn ProgramRunResult::deliver(
  self : ProgramRunResult,
  write : (String) -> Unit,
  report : (Diagnostic) -> Unit,
) -> ProgramRunResult {
  match self {
    ProgramCompleted(_, output) =>
      for line in output {
        write(line)
      }
    ProgramFailed(issues, output) => {
      for line in output {
        write(line)
      }
      for issue in issues {
        report(issue)
      }
    }
  }
  self
}