///|
struct Cmd[Msg] {
  tasks : Array[((Msg) -> Unit) -> Unit]
}

///|
pub fn[Msg] Cmd::none() -> Cmd[Msg] {
  { tasks: [] }
}

///|
pub fn[Msg] Cmd::batch(cmds : Array[Cmd[Msg]]) -> Cmd[Msg] {
  { tasks: cmds.iter().flat_map(fn(cmd) { cmd.tasks.iter() }).collect() }
}

///|
pub fn[A, B] Cmd::map(self : Cmd[A], f : (A) -> B) -> Cmd[B] {
  {
    tasks: self.tasks.map(fn(task) {
      fn(dispatch : (B) -> Unit) { task(fn(a) { dispatch(f(a)) }) }
    }),
  }
}

///|
pub fn[Msg] Cmd::task(task : ((Msg) -> Unit) -> Unit) -> Cmd[Msg] {
  { tasks: [task] }
}

///|
fn[Msg] Cmd::run(self : Cmd[Msg], dispatch : (Msg) -> Unit) -> Unit {
  self.tasks.each(fn(task) { task(dispatch) })
}

///|
/// Send a message into a child component's update loop via its Handle.
pub fn[CMsg, Msg] Cmd::send(handle : Handle[CMsg], msg : CMsg) -> Cmd[Msg] {
  { tasks: [fn(_dispatch) { (handle.dispatch.val)(msg) }] }
}

///|
fn js_callback(f : () -> Unit) -> @webapi.Function {
  @webapi.Function::new(fn(_) {
    f()
    @webapi.JsValue::undefined()
  })
}

///|
/// Dispatch a message after a delay in milliseconds.
pub fn[Msg] Cmd::after(ms : Int, msg : Msg) -> Cmd[Msg] {
  Cmd::task(fn(dispatch) {
    @webapi.window().set_timeout(
      js_callback(fn() { dispatch(msg) }),
      timeout=ms,
      [],
    )
    |> ignore
  })
}

///|
fn error_message(reason : @webapi.JsValue) -> String {
  if reason.is_null() {
    return reason.to_string()
  }
  let obj : @webapi.JsObject = reason.unsafe_into()
  let msg = obj.get("message")
  if msg.is_null() {
    reason.to_string()
  } else {
    msg.to_string()
  }
}

///|
/// Perform an HTTP GET request. Dispatches `on_result(Ok(body))` on success
/// or `on_result(Err(message))` on failure.
pub fn[Msg] Cmd::http_get(
  url : String,
  on_result : (Result[String, String]) -> Msg,
) -> Cmd[Msg] {
  Cmd::task(fn(dispatch) {
    @webapi.window()
    .fetch(url)
    .then(fn(response : @webapi.Response) {
      if response.ok() {
        response
        .text()
        .then(fn(body : String) { dispatch(on_result(Ok(body))) })
        .catch_(fn(reason : @webapi.JsValue) {
          dispatch(on_result(Err(error_message(reason))))
        })
        |> ignore
      } else {
        dispatch(on_result(Err("HTTP " + response.status().to_string())))
      }
    })
    .catch_(fn(reason : @webapi.JsValue) {
      dispatch(on_result(Err(error_message(reason))))
    })
    |> ignore
  })
}