///|
using @interface {trait Scheduler}

///|
using @common {type Keyboard, type Mouse, type Scroll, type Viewport}

///|
using @cmd {type Cmd}

///|
/// A long-lived subscription managed by the Rabbita runtime.
///
/// Use subscriptions to listen to external signals such as timers, window
/// resize, scrolling, keyboard input, or visibility changes.
struct Sub(Map[String, @interface.CustomSubscription])

///|
fn custom_subscription(key : String, load : (&Scheduler) -> () -> Unit) -> Sub {
  Map::from_array([(key, load)])
}

///|
pub impl @interface.Subscription for Sub with to_map(self) {
  self.0
}

///|
fn add_cmd(scheduler : &Scheduler, cmd : Cmd) -> Unit {
  scheduler.add_cmds(cmd.to_array())
}

///|
fn event_subscription(
  key : String,
  target : @dom.EventTarget,
  event_name : String,
  handler : (@dom.Event) -> Cmd,
) -> Sub {
  custom_subscription(key, scheduler => {
    let listener : @dom.Listener = event => add_cmd(scheduler, handler(event))
    target.add_event_listener(event_name, listener)
    fn() { target.remove_event_listener(event_name, listener) }
  })
}

///|
fn keyboard_from_dom(event : @dom.KeyboardEvent) -> Keyboard {
  Keyboard::new(
    key=event.key(),
    code=event.code(),
    alt_key=event.alt_key(),
    ctrl_key=event.ctrl_key(),
    shift_key=event.shift_key(),
    meta_key=event.meta_key(),
    is_composing=event.is_composing(),
    repeat=event.repeat(),
    location=event.location(),
  )
}

///|
fn mouse_from_dom(event : @dom.MouseEvent) -> Mouse {
  Mouse::new(
    screen={ x: event.get_screen_x(), y: event.get_screen_y() },
    offset={ x: event.get_offset_x(), y: event.get_offset_y() },
    client={ x: event.get_client_x(), y: event.get_client_y() },
  )
}

///|
fn window_scroll() -> Scroll {
  let root = @dom.document().query_selector("html").unwrap()
  Scroll::new(
    offset={ x: @dom.window().scroll_x(), y: @dom.window().scroll_y() },
    width=root.get_scroll_width().to_int(),
    height=root.get_scroll_height().to_int(),
  )
}

///|
/// Repeatedly enqueue `cmd` every `ms` milliseconds.
pub fn every(ms : Int, cmd : @cmd.Cmd) -> Sub {
  custom_subscription("every(\{ms})", scheduler => {
    let id : Int = @dom.window().set_interval(() => add_cmd(scheduler, cmd), ms)
    fn() { @dom.window().clear_interval(id) }
  })
}

///|
/// Subscribe to window resize events.
///
/// The handler receives the latest viewport width and height.
pub fn on_resize(msg : @cmd.Dispatch[Viewport]) -> Sub {
  event_subscription("on_resize", @dom.window().as_event_target(), "resize", event => {
    ignore(event)
    msg({
      width: @dom.window().inner_width(),
      height: @dom.window().inner_height(),
    })
  })
}

///|
/// Subscribe to `requestAnimationFrame` ticks.
///
/// The handler receives the browser timestamp for each frame while the
/// subscription is active.
pub fn on_animation_frame(msg : @cmd.Dispatch[Double]) -> Sub {
  custom_subscription("on_animation_frame", scheduler => {
    let mut active = true
    let mut request_id : Double? = None

    fn queue_next() {
      request_id = Some(
        @dom.window().request_animation_frame(timestamp => {
          guard active else { return }
          add_cmd(scheduler, msg(timestamp))
          queue_next()
        }),
      )
    }

    queue_next()
    fn() {
      active = false
      guard request_id is Some(id) else { return }
      @dom.window().cancel_animation_frame(id)
    }
  })
}

///|
/// Subscribe to document `keydown` events.
///
/// The handler receives a normalized `Keyboard` value from `@common`.
pub fn on_key_down(msg : @cmd.Dispatch[Keyboard]) -> Sub {
  event_subscription(
    "on_key_down",
    @dom.document().as_event_target(),
    "keydown",
    event => msg(keyboard_from_dom(event.to_keyboard_event().unwrap())),
  )
}

///|
/// Subscribe to document `keyup` events.
///
/// The handler receives a normalized `Keyboard` value from `@common`.
pub fn on_key_up(msg : @cmd.Dispatch[Keyboard]) -> Sub {
  event_subscription("on_key_up", @dom.document().as_event_target(), "keyup", event => {
    msg(keyboard_from_dom(event.to_keyboard_event().unwrap()))
  })
}

///|
/// Subscribe to document visibility changes.
///
/// The handler receives `true` when the document is hidden, and `false`
/// when it becomes visible again.
pub fn on_visibility_change(msg : @cmd.Dispatch[Bool]) -> Sub {
  event_subscription(
    "on_visibility_change",
    @dom.document().as_event_target(),
    "visibilitychange",
    _ => msg(@dom.document().hidden()),
  )
}

///|
/// Subscribe to document `mousemove` events.
///
/// The handler receives a normalized `Mouse` value from `@common`.
pub fn on_mouse_move(msg : @cmd.Dispatch[Mouse]) -> Sub {
  event_subscription(
    "on_mouse_move",
    @dom.document().as_event_target(),
    "mousemove",
    event => msg(mouse_from_dom(event.to_mouse_event().unwrap())),
  )
}

///|
/// Subscribe to window scroll events.
///
/// This reports page-level scrolling on `window`, not scrolling of an inner
/// element. The handler receives the current scroll offset and document scroll
/// size as a `Scroll` value.
pub fn on_scroll(msg : @cmd.Dispatch[Scroll]) -> Sub {
  event_subscription("on_scroll", @dom.window().as_event_target(), "scroll", event => {
    ignore(event)
    msg(window_scroll())
  })
}

///|
/// A subscription that does nothing.
pub let none : Sub = {}

///|
/// Combine multiple subscriptions into one.
///
/// If multiple subscriptions use the same internal key, the later one wins.
pub fn batch(xs : Array[Sub]) -> Sub {
  let r = {}
  for x in xs {
    for k, v in x.0 {
      r[k] = v
    }
  }
  r
}