///|
priv struct SubDef[Msg] {
  key : String
  identity : String
  start : ((Msg) -> Unit) -> () -> Unit
}

///|
struct Sub[Msg] {
  subs : Array[SubDef[Msg]]
}

///|
let sub_identity_counter : Ref[Int] = Ref::new(0)

///|
fn fresh_sub_identity(prefix : String) -> String {
  let next_id = sub_identity_counter.val
  sub_identity_counter.val = next_id + 1
  prefix + ":" + next_id.to_string()
}

///|
fn[Msg] tracked_sub(
  key : String,
  identity : String,
  start : ((Msg) -> Unit) -> () -> Unit,
) -> Sub[Msg] {
  { subs: [{ key, identity, start }] }
}

///|
pub fn[Msg] Sub::none() -> Sub[Msg] {
  { subs: [] }
}

///|
pub fn[Msg] Sub::batch(subs : Array[Sub[Msg]]) -> Sub[Msg] {
  { subs: subs.iter().flat_map(fn(sub) { sub.subs.iter() }).collect() }
}

///|
pub fn[A, B] Sub::map(self : Sub[A], f : (A) -> B) -> Sub[B] {
  {
    subs: self.subs.map(fn(sd) {
      {
        key: sd.key,
        identity: sd.identity,
        start: fn(dispatch : (B) -> Unit) {
          (sd.start)(fn(a) { dispatch(f(a)) })
        },
      }
    }),
  }
}

///|
pub fn[Msg] Sub::sub(
  key : String,
  start : ((Msg) -> Unit) -> () -> Unit,
) -> Sub[Msg] {
  tracked_sub(key, fresh_sub_identity("custom"), start)
}

///|
/// Subscribe to a recurring timer. Fires `to_msg()` every `ms` milliseconds.
pub fn[Msg] Sub::every(ms : Int, key : String, to_msg : () -> Msg) -> Sub[Msg] {
  tracked_sub(key, "every:" + ms.to_string(), fn(dispatch) {
    let id = @webapi.window().set_interval(
      js_callback(fn() { dispatch(to_msg()) }),
      timeout=ms,
      [],
    )
    fn() { @webapi.window().clear_interval(id~) }
  })
}

///|
/// Subscribe to `keydown` events on the document.
/// When `prevent_default` is given, only keys matching the predicate will
/// have their default browser action suppressed (e.g., scrolling, button
/// activation). Without it, no defaults are prevented.
pub fn[Msg] Sub::on_key_down(
  key : String,
  to_msg : (String) -> Msg,
  prevent_default? : (String) -> Bool,
) -> Sub[Msg] {
  tracked_sub(key, "document:keydown", fn(dispatch) {
    let handler : (@webapi.Event) -> Unit = fn(e) {
      let ke : @webapi.KeyboardEvent = e.unsafe_into()
      let pressed = ke.key()
      match prevent_default {
        Some(pred) => if pred(pressed) { e.prevent_default() }
        None => ()
      }
      dispatch(to_msg(pressed))
    }
    @webapi.document().add_event_listener("keydown", handler)
    fn() { @webapi.document().remove_event_listener("keydown", handler) }
  })
}

///|
/// Subscribe to `resize` events on the window.
pub fn[Msg] Sub::on_window_resize(
  key : String,
  to_msg : (Int, Int) -> Msg,
) -> Sub[Msg] {
  tracked_sub(key, "window:resize", fn(dispatch) {
    let handler : (@webapi.Event) -> Unit = fn(_e) {
      dispatch(
        to_msg(@webapi.window().inner_width(), @webapi.window().inner_height()),
      )
    }
    @webapi.window().add_event_listener("resize", handler)
    fn() { @webapi.window().remove_event_listener("resize", handler) }
  })
}