///|
pub(all) enum EventDomain {
  Frame
  Fixed
  Render
} derive(Eq, Debug)

///|
pub(all) struct EventReader[T] {
  last_seen_by_world : Map[UInt, Int]
  marker : T?
}

///|
pub(all) struct EventWriter[T] {
  events : Events[T]
}

///|
pub(all) struct ObserverHandle(Int) derive(Eq, Debug, Hash)

///|
pub(all) struct Trigger[T] {
  event : T
  target : @entity.Entity?
}

///|
priv struct EventRecord[T] {
  id : Int
  event : T
}

///|
priv struct ObserverEntry[T] {
  target : @entity.Entity?
  callback : (Trigger[T]) -> Unit
}

///|
struct EventWorldStore[T] {
  mut next_event_id : Int
  mut next_observer_id : Int
  mut older : Array[EventRecord[T]]
  mut newer : Array[EventRecord[T]]
  observers : Map[ObserverHandle, ObserverEntry[T]]
  observer_queue : Array[Trigger[T]]
}

///|
pub struct Events[T] {
  id : UInt
  domain : EventDomain
  stores : Map[UInt, EventWorldStore[T]]
  marker : T?
}

///|
priv struct RegisteredBus {
  id : UInt
  domain : EventDomain
  update : () -> Unit
  dispatch : () -> Unit
}

///|
let registered_buses_by_world : Map[UInt, Array[RegisteredBus]] = Map([])

///|
let next_event_bus_id : Ref[UInt] = { val: 1U }

///|
fn allocate_event_bus_id() -> UInt {
  let id = next_event_bus_id.val
  next_event_bus_id.val += 1U
  id
}

///|
fn current_world_id() -> UInt {
  @ecs.require_current_world().id()
}

///|
fn[T] world_store(events : Events[T]) -> EventWorldStore[T] {
  let world_id = current_world_id()
  events.stores.get_or_init(world_id, () => {
    next_event_id: 0,
    next_observer_id: 0,
    older: [],
    newer: [],
    observers: Map([]),
    observer_queue: [],
  })
}

///|
pub fn[T] EventReader::EventReader() -> EventReader[T] {
  { last_seen_by_world: Map([]), marker: None }
}

///|
pub fn[T] EventReader::read(
  self : EventReader[T],
  events : Events[T],
) -> Array[T] {
  let world_id = current_world_id()
  let last_seen = self.last_seen_by_world.get(world_id).unwrap_or(-1)
  let store = world_store(events)
  let values : Array[T] = []
  let mut latest_id = last_seen
  for record in store.older {
    if record.id > last_seen {
      values.push(record.event)
      latest_id = record.id
    }
  }
  for record in store.newer {
    if record.id > last_seen {
      values.push(record.event)
      latest_id = record.id
    }
  }
  self.last_seen_by_world.set(world_id, latest_id)
  values
}

///|
pub fn[T] EventWriter::EventWriter(events : Events[T]) -> EventWriter[T] {
  { events, }
}

///|
pub fn[T] EventWriter::send(self : EventWriter[T], event : T) -> Unit {
  self.events.send(event)
}

///|
pub fn[T] EventWriter::trigger(
  self : EventWriter[T],
  event : T,
  target? : @entity.Entity?,
) -> Unit {
  self.events.trigger(event, target.unwrap_or(None))
}

///|
pub fn[T] Events::Events(domain? : EventDomain = Frame) -> Events[T] {
  { id: allocate_event_bus_id(), domain, stores: Map([]), marker: None }
}

///|
pub fn[T] Events::register_to_world(
  self : Events[T],
  world : @ecs.World,
) -> Unit {
  let world_id = world.id()
  let buses = registered_buses_by_world.get(world_id).unwrap_or([])
  for bus in buses {
    if bus.id == self.id {
      return
    }
  }
  buses.push({
    id: self.id,
    domain: self.domain,
    update: fn() { self.update() },
    dispatch: fn() { self.dispatch_observers() },
  })
  registered_buses_by_world.set(world_id, buses)
}

///|
pub fn[T] Events::writer(self : Events[T]) -> EventWriter[T] {
  EventWriter(self)
}

///|
pub fn[T] Events::read(self : Events[T], reader : EventReader[T]) -> Array[T] {
  reader.read(self)
}

///|
pub fn[T] Events::send(self : Events[T], event : T) -> Unit {
  let store = world_store(self)
  let id = store.next_event_id
  store.next_event_id += 1
  store.newer.push({ id, event })
}

///|
pub fn[T] Events::trigger(
  self : Events[T],
  event : T,
  target : @entity.Entity?,
) -> Unit {
  let store = world_store(self)
  self.send(event)
  store.observer_queue.push({ event, target })
}

///|
pub fn[T] Events::observe_global(
  self : Events[T],
  callback : (Trigger[T]) -> Unit,
) -> ObserverHandle {
  let store = world_store(self)
  let handle = ObserverHandle(store.next_observer_id)
  store.next_observer_id += 1
  store.observers.set(handle, { target: None, callback })
  handle
}

///|
pub fn[T] Events::observe_entity(
  self : Events[T],
  entity : @entity.Entity,
  callback : (Trigger[T]) -> Unit,
) -> ObserverHandle {
  let store = world_store(self)
  let handle = ObserverHandle(store.next_observer_id)
  store.next_observer_id += 1
  store.observers.set(handle, { target: Some(entity), callback })
  handle
}

///|
pub fn[T] Events::unobserve(self : Events[T], handle : ObserverHandle) -> Unit {
  let store = world_store(self)
  store.observers.remove(handle)
}

///|
pub fn[T] Events::clear(self : Events[T]) -> Unit {
  let store = world_store(self)
  store.older.clear()
  store.newer.clear()
  store.observer_queue.clear()
}

///|
pub fn[T] Events::update(self : Events[T]) -> Unit {
  let store = world_store(self)
  store.older = store.newer
  store.newer = []
}

///|
pub fn[T] Events::dispatch_observers(self : Events[T]) -> Unit {
  let store = world_store(self)
  for item in store.observer_queue {
    for _handle, observer in store.observers {
      let deliver = match observer.target {
        None => true
        Some(entity) =>
          match item.target {
            Some(target) => target == entity
            None => false
          }
      }
      if deliver {
        (observer.callback)(item)
      }
    }
  }
  store.observer_queue.clear()
}

///|
pub fn[T] read(reader : EventReader[T], events : Events[T]) -> Array[T] {
  reader.read(events)
}

///|
pub fn[T] send(events : Events[T], event : T) -> Unit {
  events.send(event)
}

///|
pub fn[T] trigger(
  events : Events[T],
  event : T,
  target? : @entity.Entity?,
) -> Unit {
  events.trigger(event, target.unwrap_or(None))
}

///|
pub fn[T] observe_global(
  events : Events[T],
  callback : (Trigger[T]) -> Unit,
) -> ObserverHandle {
  events.observe_global(callback)
}

///|
pub fn[T] observe_entity(
  events : Events[T],
  entity : @entity.Entity,
  callback : (Trigger[T]) -> Unit,
) -> ObserverHandle {
  events.observe_entity(entity, callback)
}

///|
pub fn[T] unobserve(events : Events[T], handle : ObserverHandle) -> Unit {
  events.unobserve(handle)
}

///|
fn update_domain(domain : EventDomain) -> Unit {
  let world_id = current_world_id()
  for bus in registered_buses_by_world.get(world_id).unwrap_or([]) {
    if bus.domain == domain {
      (bus.update)()
    }
  }
}

///|
pub fn event_frame_update_system(_delta : Double) -> Unit {
  update_domain(Frame)
}

///|
pub fn event_fixed_update_system(_delta : Double) -> Unit {
  update_domain(Fixed)
}

///|
pub fn event_render_update_system(_delta : Double) -> Unit {
  update_domain(Render)
}

///|
pub fn event_observer_dispatch_system(_delta : Double) -> Unit {
  let world_id = current_world_id()
  for bus in registered_buses_by_world.get(world_id).unwrap_or([]) {
    (bus.dispatch)()
  }
}