///|
/// Proton's event loop, handed to `moonbitlang/async` so the two can share the
/// main thread.
///
/// AppKit and its peers insist on owning the process's main thread, and
/// `moonbitlang/async` wants a thread for its own loop. Installing this loop
/// settles the argument: every line of MoonBit, this `poll` included, keeps
/// running on the main thread, and `async` moves only its waiting half onto a
/// thread of its own.
priv struct ProtonEventLoop {
  signal : @core.RuntimeWakeSignal
  mut installed : Bool
  mut install_error : AppRunError?
}

///|
/// The process's host loop. There is one main thread, so there is one of these.
///
/// It is built at startup rather than on demand because `poll` may not
/// allocate a notification target the first time `async` calls it: the
/// scheduler is already running by then, and a waiter that missed the object it
/// was supposed to wait on would never be woken.
let host_event_loop : ProtonEventLoop = ProtonEventLoop::{
  signal: @core.RuntimeWakeSignal::new(),
  installed: false,
  install_error: None,
}

///|
/// Installs the loop before anything else in the process runs.
///
/// `fn init` is the one place guaranteed to run before `moonbitlang/async`
/// starts its loop, which is the deadline `set_external_event_loop` imposes --
/// installing afterwards aborts. Doing it here rather than asking every
/// application to spell it out as the first line of `async fn main` means the
/// requirement cannot be forgotten, and nothing in the language would have
/// caught forgetting it.
///
/// A failure is kept, not raised. `fn init` cannot raise, aborting would take
/// down a process that may never open a window, and `App::run` is the place
/// where a caller can be told properly.
fn init {
  install_event_loop() catch {
    error => host_event_loop.install_error = Some(error)
  }
}

///|
/// Hands Proton's event loop to `moonbitlang/async`. Call once, from `fn init`:
/// `async` aborts on a second install.
fn install_event_loop() -> Unit raise AppRunError {
  // Begin first. A failure here leaves nothing installed, whereas installing
  // first would hand `async` a loop that never started and turn every later
  // `poll` into the same error.
  @native.host_loop_begin() catch {
    error => raise native_run_error("start the host event loop", error)
  }
  @async.set_external_event_loop(host_event_loop)
  host_event_loop.installed = true
}

///|
/// Fails unless Proton's event loop is running.
///
/// Everything the facade does with a native runtime depends on it: the loop is
/// what drives the platform and what turns native work into a notification the
/// scheduler can wait on. The error is the one `fn init` hit, because that is
/// the only way this can be reached.
fn require_event_loop() -> Unit raise AppRunError {
  guard !host_event_loop.installed else { return }
  match host_event_loop.install_error {
    Some(error) => raise error
    None => raise EventLoopError("Proton's event loop was never installed")
  }
}

///|
/// The notification signal `poll` raises when native work arrives.
fn host_event_loop_signal() -> @core.RuntimeWakeSignal {
  host_event_loop.signal
}

///|
impl @extloop.ExternalEventLoop for ProtonEventLoop with fn poll(self, timeout?) {
  let ready = @native.host_loop_poll(timeout_ms?=timeout)
  // Only a mask with something in it is worth a notification. `poll` runs on
  // every turn of `async`'s loop, so notifying unconditionally would rouse the
  // runtime pump on every timer tick in the application and make it re-poll
  // native queues that nothing has touched.
  if !ready.is_timeout() {
    self.signal.notify()
  }
}

///|
impl @extloop.ExternalEventLoop for ProtonEventLoop with fn get_wakeup_callback_for_foreign_thread(
  _self,
) {
  // Runs on `async`'s waiting thread, where the contract forbids anything but a
  // direct C call: no allocation, and nothing that touches a refcount.
  () => @native.signal_wakeup()
}

///|
impl @extloop.ExternalEventLoop for ProtonEventLoop with fn terminate(_self) {
  @native.host_loop_end()
}