///|
/// Runs the configured app through the native Proton runtime.
pub async fn App::run(self : App) -> Result[Unit, String] {
  @async.sleep(0)
  match self.validation_error() {
    Some(error) => return Err(error)
    None => ()
  }
  match self.resolved_manifest() {
    Err(error) => Err(error)
    Ok(manifest) =>
      run_manifest(
        manifest,
        self.command_extensions,
        app_config_base_dir(self.file_path),
      )
  }
}

///|
/// Runs the configured app and aborts with the error message on failure.
pub async fn App::run_or_abort(self : App) -> Unit {
  match self.run() {
    Ok(_) => ()
    Err(error) => abort(error)
  }
}

///|
async fn run_manifest(
  manifest : @manifest.AppManifest,
  command_extensions : Map[String, @proton_command.AppCommandExtensionSpec],
  base_dir : String?,
) -> Result[Unit, String] {
  let runtime = match
    @native.Runtime::new(
      config=@native.RuntimeConfig::bundled(
        remote_debugging_port=debug_port(manifest.debug),
      ),
    ) {
    Ok(runtime) => runtime
    Err(error) => return Err(native_error_message("create runtime", error))
  }
  let window = match
    @native.Window::new(
      runtime,
      config=@native.WindowConfig::new(
        title=manifest.window.title,
        width=manifest.window.width,
        height=manifest.window.height,
        initial_url="about:blank",
      ),
    ) {
    Ok(window) => window
    Err(error) => {
      ignore(runtime.destroy())
      return Err(native_error_message("create window", error))
    }
  }

  let command_host = match
    build_command_host(manifest, command_extensions, base_dir) {
    Err(error) => {
      ignore(window.destroy())
      ignore(runtime.destroy())
      return Err(error)
    }
    Ok(host) => host
  }
  let command_proxy_script = match
    build_command_proxy_script(manifest, command_extensions) {
    Err(error) => {
      close_command_host(command_host)
      ignore(window.destroy())
      ignore(runtime.destroy())
      return Err(error)
    }
    Ok(script) => script
  }
  match command_host {
    Some(host) => {
      let ops = host.registered_ops()
      match
        native_unit_result(
          "install bridge",
          window.install_bridge(@native.BridgeConfig::new(ops~)),
        ) {
        Err(error) => {
          host.close()
          ignore(window.destroy())
          ignore(runtime.destroy())
          return Err(error)
        }
        Ok(_) => install_extension_event_sender(window, host)
      }
    }
    None => ()
  }
  match load_entry_after_create(window, manifest.entry, command_proxy_script) {
    Err(error) => {
      close_command_host(command_host)
      ignore(window.destroy())
      ignore(runtime.destroy())
      Err(error)
    }
    Ok(_) =>
      match native_unit_result("show window", window.show()) {
        Err(error) => {
          close_command_host(command_host)
          ignore(window.destroy())
          ignore(runtime.destroy())
          Err(error)
        }
        Ok(_) => {
          let run_result = match command_host {
            Some(host) => run_bridge_pump(runtime, window, host)
            None => run_window_pump(runtime)
          }
          close_command_host(command_host)
          let destroy_result = native_unit_result(
            "destroy runtime",
            runtime.destroy(),
          )
          match run_result {
            Err(error) => Err(error)
            Ok(_) => destroy_result
          }
        }
      }
  }
}

///|
fn app_config_base_dir(file_path : String?) -> String? {
  match file_path {
    Some(path) => Some((path : @mbpath.Path).dirname().to_string())
    None => None
  }
}

///|
fn close_command_host(host : @core.AppCommandHost?) -> Unit {
  match host {
    Some(value) => value.close()
    None => ()
  }
}

///|
async fn run_window_pump(runtime : @native.Runtime) -> Result[Unit, String] {
  let mut closed = false
  let mut idle_delay = 0
  let mut runtime_wait_supported = true
  while !closed {
    let mut did_work = false
    match runtime.do_message_loop_work() {
      Err(error) => return Err(native_error_message("pump runtime", error))
      Ok(_) => ()
    }

    let mut polling_events = true
    while polling_events {
      match runtime.poll_event() {
        Err(error) => return Err(native_error_message("poll event", error))
        Ok(None) => polling_events = false
        Ok(Some(event)) => {
          did_work = true
          if event.is_window_closed() {
            closed = true
          }
        }
      }
    }

    if did_work {
      idle_delay = 0
      @async.sleep(0)
    } else {
      match wait_for_runtime_idle(runtime, idle_delay, runtime_wait_supported) {
        Err(error) => return Err(error)
        Ok((next_delay, next_wait_supported)) => {
          idle_delay = next_delay
          runtime_wait_supported = next_wait_supported
        }
      }
    }
  }
  Ok(())
}

///|
async fn run_bridge_pump(
  runtime : @native.Runtime,
  window : @native.Window,
  host : @core.AppCommandHost,
) -> Result[Unit, String] {
  @async.with_task_group(group => {
    let mut closed = false
    let mut idle_delay = 0
    let mut runtime_wait_supported = true
    let mut pending : Array[BridgeDispatchTask] = []
    while !closed {
      let mut did_work = false
      match runtime.do_message_loop_work() {
        Err(error) => return Err(native_error_message("pump runtime", error))
        Ok(_) => ()
      }

      let mut polling_events = true
      while polling_events {
        match runtime.poll_event() {
          Err(error) => return Err(native_error_message("poll event", error))
          Ok(None) => polling_events = false
          Ok(Some(event)) => {
            did_work = true
            if event.is_window_closed() {
              closed = true
            }
          }
        }
      }

      let mut polling_bridge = true
      while polling_bridge {
        match runtime.poll_bridge_request() {
          Err(error) =>
            return Err(native_error_message("poll bridge request", error))
          Ok(None) => polling_bridge = false
          Ok(Some(request)) => {
            did_work = true
            let request_id = request.request_id()
            let task = group.spawn(
              () => dispatch_bridge_request(window, host, request),
              no_wait=true,
              allow_failure=true,
            )
            pending.push(BridgeDispatchTask::{ request_id, task })
          }
        }
      }

      let remaining : Array[BridgeDispatchTask] = []
      for item in pending {
        let completed = item.task.try_wait() catch {
          _ =>
            Some(
              @native.BridgeResponse::Err(
                request_id=item.request_id,
                code="handler_failed",
                message="bridge handler task failed",
              ),
            )
        }
        match completed {
          Some(response) => {
            did_work = true
            match runtime.respond_bridge_request(response) {
              Err(error) =>
                if !is_stale_bridge_response_error(error) {
                  return Err(
                    native_error_message("respond bridge request", error),
                  )
                }
              Ok(_) => ()
            }
          }
          None => remaining.push(item)
        }
      }
      pending = remaining

      if did_work {
        idle_delay = 0
        @async.sleep(0)
      } else if pending.length() > 0 {
        idle_delay = 0
        @async.sleep(1)
      } else {
        match
          wait_for_runtime_idle(runtime, idle_delay, runtime_wait_supported) {
          Err(error) => return Err(error)
          Ok((next_delay, next_wait_supported)) => {
            idle_delay = next_delay
            runtime_wait_supported = next_wait_supported
          }
        }
      }
    }
    for item in pending {
      item.task.cancel()
    }
    Ok(())
  })
}

///|
async fn wait_for_runtime_idle(
  runtime : @native.Runtime,
  idle_delay : Int,
  runtime_wait_supported : Bool,
) -> Result[(Int, Bool), String] {
  if runtime_wait_supported {
    match runtime.wait(interest_mask=@native.runtime_wait_all, timeout_ms=16) {
      Ok(_) => Ok((0, true))
      Err(error) =>
        if is_runtime_wait_unsupported(error) {
          sleep_with_idle_backoff(idle_delay).map(fn(next_delay) {
            (next_delay, false)
          })
        } else {
          Err(native_error_message("wait runtime", error))
        }
    }
  } else {
    sleep_with_idle_backoff(idle_delay).map(fn(next_delay) {
      (next_delay, false)
    })
  }
}

///|
async fn sleep_with_idle_backoff(idle_delay : Int) -> Result[Int, String] {
  let delay = next_idle_delay(idle_delay)
  @async.sleep(delay)
  Ok(delay)
}

///|
fn is_runtime_wait_unsupported(error : @native.NativeError) -> Bool {
  error.status == proton_err_unsupported
}

///|
let proton_err_unsupported = -8

///|
fn is_stale_bridge_response_error(error : @native.NativeError) -> Bool {
  (error.status == -2 && error.message.contains("no longer pending")) ||
  (
    error.status == -7 &&
    error.message.contains("failed to send bridge response to renderer")
  )
}

///|
fn next_idle_delay(current : Int) -> Int {
  if current <= 0 {
    1
  } else if current >= 16 {
    16
  } else {
    let next = current * 2
    if next > 16 {
      16
    } else {
      next
    }
  }
}