///|
pub(all) struct KeyboardEvent {
  window_id : UInt
  keyboard_id : UInt
  scancode : Int
  keycode : UInt
  modifiers : UInt16
  repeat : Bool
} derive(Debug, Eq)

///|
pub(all) struct MouseMotionEvent {
  window_id : UInt
  mouse_id : UInt
  state : UInt
  x : Float
  y : Float
  xrel : Float
  yrel : Float
} derive(Debug, Eq)

///|
pub(all) struct MouseButtonEvent {
  window_id : UInt
  mouse_id : UInt
  button : Byte
  clicks : Byte
  x : Float
  y : Float
} derive(Debug, Eq)

///|
pub(all) struct WindowEvent {
  window_id : UInt
  name : WindowEventName
  size : Size?
} derive(Debug, Eq)

///|
pub(all) struct TextInputEvent {
  text : String
} derive(Debug, Eq)

///|
pub(all) enum WindowEventName {
  Exposed
  Resized
  PixelSizeChanged
  CloseRequested
} derive(Debug, Eq)

///|
pub(all) enum Event {
  Quit
  KeyDown(KeyboardEvent)
  KeyUp(KeyboardEvent)
  MouseMotion(MouseMotionEvent)
  MouseButtonDown(MouseButtonEvent)
  MouseButtonUp(MouseButtonEvent)
  TextInput(TextInputEvent)
  WindowExposed(WindowEvent)
  WindowResized(WindowEvent)
  WindowPixelSizeChanged(WindowEvent)
  WindowCloseRequested(WindowEvent)
  Other(UInt)
} derive(Debug, Eq)

///|
struct EventListener {
  raw : @sys.EventWatch
}

///|
let call_event_watch_callback : FuncRef[((Bytes) -> Unit, Bytes) -> Unit] = fn(
  callback,
  event,
) {
  callback(event)
}

///|
pub fn poll_event() -> Event? {
  let raw = @sys.Event()
  if @sys.poll_event(raw) {
    Some(Event::from_raw(raw))
  } else {
    None
  }
}

///|
fn wait_event_timeout(timeout_ms : Int) -> Event? {
  let raw = @sys.Event()
  if @sys.wait_event_timeout(raw, timeout_ms) {
    Some(Event::from_raw(raw))
  } else {
    None
  }
}

///|
fn Event::from_raw(raw : @sys.Event) -> Event {
  let event_type = raw.event_type()
  match event_type {
    @sys.EVENT_QUIT => Quit
    @sys.EVENT_KEY_DOWN => KeyDown(KeyboardEvent::from_raw(raw))
    @sys.EVENT_KEY_UP => KeyUp(KeyboardEvent::from_raw(raw))
    @sys.EVENT_MOUSE_MOTION => MouseMotion(MouseMotionEvent::from_raw(raw))
    @sys.EVENT_MOUSE_BUTTON_DOWN =>
      MouseButtonDown(MouseButtonEvent::from_raw(raw))
    @sys.EVENT_MOUSE_BUTTON_UP => MouseButtonUp(MouseButtonEvent::from_raw(raw))
    @sys.EVENT_TEXT_INPUT => TextInput(TextInputEvent::from_raw(raw))
    @sys.EVENT_WINDOW_EXPOSED =>
      WindowExposed(WindowEvent::from_raw(raw, Exposed))
    @sys.EVENT_WINDOW_RESIZED =>
      WindowResized(WindowEvent::from_raw(raw, Resized))
    @sys.EVENT_WINDOW_PIXEL_SIZE_CHANGED =>
      WindowPixelSizeChanged(WindowEvent::from_raw(raw, PixelSizeChanged))
    @sys.EVENT_WINDOW_CLOSE_REQUESTED =>
      WindowCloseRequested(WindowEvent::from_raw(raw, CloseRequested))
    _ => Other(event_type)
  }
}

///|
fn KeyboardEvent::from_raw(raw : @sys.Event) -> KeyboardEvent {
  {
    window_id: raw.key_window_id(),
    keyboard_id: raw.key_keyboard_id(),
    scancode: raw.key_scancode(),
    keycode: raw.key_keycode(),
    modifiers: raw.key_modifiers(),
    repeat: raw.key_repeat(),
  }
}

///|
fn MouseMotionEvent::from_raw(raw : @sys.Event) -> MouseMotionEvent {
  {
    window_id: raw.motion_window_id(),
    mouse_id: raw.motion_mouse_id(),
    state: raw.motion_state(),
    x: raw.motion_x(),
    y: raw.motion_y(),
    xrel: raw.motion_xrel(),
    yrel: raw.motion_yrel(),
  }
}

///|
fn MouseButtonEvent::from_raw(raw : @sys.Event) -> MouseButtonEvent {
  {
    window_id: raw.button_window_id(),
    mouse_id: raw.button_mouse_id(),
    button: raw.button_button(),
    clicks: raw.button_clicks(),
    x: raw.button_x(),
    y: raw.button_y(),
  }
}

///|
fn WindowEvent::from_raw(
  raw : @sys.Event,
  name : WindowEventName,
) -> WindowEvent {
  let size = match name {
    Resized | PixelSizeChanged =>
      Some(Size::{ width: raw.window_data1(), height: raw.window_data2() })
    Exposed | CloseRequested => None
  }
  { window_id: raw.window_window_id(), name, size }
}

///|
fn TextInputEvent::from_raw(raw : @sys.Event) -> TextInputEvent {
  { text: @utf8.decode_lossy(raw.text_text()) }
}

///|
fn WindowEventName::matches(
  self : Self,
  event : Event,
  window_id : UInt,
) -> WindowEvent? {
  let window_event = match event {
    WindowExposed(event) if self == Exposed => Some(event)
    WindowResized(event) if self == Resized => Some(event)
    WindowPixelSizeChanged(event) if self == PixelSizeChanged => Some(event)
    WindowCloseRequested(event) if self == CloseRequested => Some(event)
    _ => None
  }
  match window_event {
    Some(event) if event.window_id == window_id => Some(event)
    _ => None
  }
}

///|
pub fn EventListener::dispose(self : Self) -> Unit {
  @sys.remove_event_watch(self.raw)
}

///|
pub fn add_event_watch(
  callback : (Event) -> Unit,
) -> EventListener raise SdlError {
  let raw_callback = fn(raw_event) {
    callback(Event::from_raw(@sys.Event::from_bytes(raw_event)))
  }
  let raw = @sys.add_event_watch(call_event_watch_callback, raw_callback)
  if raw.is_null() {
    raise_last_error("SDL_AddEventWatch")
  }
  { raw, }
}

///|
pub fn Window::on(
  self : Self,
  name : WindowEventName,
  callback : (WindowEvent) -> Unit,
) -> EventListener raise SdlError {
  let window_id = self.id()
  add_event_watch(fn(event) {
    if name.matches(event, window_id) is Some(window_event) {
      callback(window_event)
    }
  })
}

///|
pub fn delay(milliseconds : UInt) -> Unit {
  @sys.delay(milliseconds)
}