///|
/// Semantic kind of the scoped outgoing events. Monaco's const-enum values are
/// `4` for ViewZones and `7` for cursor state; MoonBit deliberately exposes no
/// numeric ABI for these variants.
pub(all) enum OutgoingViewModelEventKind {
  ViewZonesChanged
  HiddenAreasChanged
  CursorStateChanged
  ModelTokensChanged
} derive(Eq, Debug)

///|
/// Complete outgoing cursor payload owned by the ViewModel. It retains the
/// earliest old state when merged and the latest new state/source/reason.
pub(all) struct CursorStateChangedEvent {
  kind : OutgoingViewModelEventKind
  old_selections : Array[@core.Selection]?
  selections : Array[@core.Selection]
  old_model_version_id : Int32
  model_version_id : Int32
  source : String
  reason : @editor_api.CursorChangeReason
  reached_max_cursor_count : Bool
} derive(Debug)

///|
/// Builds the outgoing event from one controller transition fact.
fn CursorStateChangedEvent::from_change(
  change : @cursor.CursorStateChange,
) -> CursorStateChangedEvent {
  {
    kind: CursorStateChanged,
    old_selections: change.old_selections,
    selections: change.selections,
    old_model_version_id: change.old_model_version_id,
    model_version_id: change.model_version_id,
    source: change.source,
    reason: change.reason,
    reached_max_cursor_count: change.reached_max_cursor_count,
  }
}

///|
/// Nullable ordered-selection equality from
/// `CursorStateChangedEvent._selectionsAreEqual`.
fn selections_are_equal(
  a : Array[@core.Selection]?,
  b : Array[@core.Selection]?,
) -> Bool {
  a == b
}

///|
/// An outgoing cursor event is a no-op only when both the selections and the
/// internal model versions are equal.
pub fn CursorStateChangedEvent::is_no_op(
  self : CursorStateChangedEvent,
) -> Bool {
  selections_are_equal(self.old_selections, Some(self.selections)) &&
  self.old_model_version_id == self.model_version_id
}

///|
/// Payload-free outgoing ViewZones change event. Its kind is semantic rather
/// than Monaco's numeric const-enum ordinal `4`.
pub struct ViewZonesChangedEvent {
  kind : OutgoingViewModelEventKind
} derive(Debug)

///|
/// Creates one source-shaped ViewZones change event.
pub fn ViewZonesChangedEvent::ViewZonesChangedEvent() -> ViewZonesChangedEvent {
  { kind: ViewZonesChanged, }
}

///|
/// ViewZones changes are always observable.
pub fn ViewZonesChangedEvent::is_no_op(_self : ViewZonesChangedEvent) -> Bool {
  false
}

///|
/// Payload-free outgoing hidden-area change event. It is published only after
/// the line mapping, cursor/decorations, layout, and stable viewport have been
/// updated by `ViewModel::set_hidden_areas`.
pub struct HiddenAreasChangedOutgoingEvent {
  kind : OutgoingViewModelEventKind
} derive(Debug)

///|
/// Creates one source-shaped hidden-area change event.
pub fn HiddenAreasChangedOutgoingEvent::HiddenAreasChangedOutgoingEvent() -> HiddenAreasChangedOutgoingEvent {
  { kind: HiddenAreasChanged, }
}

///|
/// A changed hidden-area mapping is always observable.
pub fn HiddenAreasChangedOutgoingEvent::is_no_op(
  _self : HiddenAreasChangedOutgoingEvent,
) -> Bool {
  false
}

///|
/// Source-shaped outgoing wrapper for one original model-token event
/// (`viewModelEventDispatcher.ts:545-551`). The wrapper deliberately retains
/// the exact event object; view-coordinate conversion belongs to the earlier
/// browser ViewEvent delivery.
pub struct ModelTokensChangedOutgoingEvent {
  kind : OutgoingViewModelEventKind
  event : @model.ModelTokensChangedEvent
}

///|
/// Builds the distinct outgoing model-token wrapper.
pub fn ModelTokensChangedOutgoingEvent::ModelTokensChangedOutgoingEvent(
  event : @model.ModelTokensChangedEvent,
) -> ModelTokensChangedOutgoingEvent {
  { kind: ModelTokensChanged, event, }
}

///|
/// Model-token outgoing events are always observable, including an empty
/// range array (`viewModelEventDispatcher.ts:552-554`).
pub fn ModelTokensChangedOutgoingEvent::is_no_op(
  _self : ModelTokensChangedOutgoingEvent,
) -> Bool {
  false
}

///|
/// Private heterogeneous outgoing-only queue. The generic mixed browser-view
/// collector/postponement layer remains intentionally absent.
priv enum PendingOutgoingEvent {
  CursorEvent(CursorStateChangedEvent)
  ViewZonesEvent(ViewZonesChangedEvent)
  HiddenAreasEvent(HiddenAreasChangedOutgoingEvent)
  ModelTokensEvent(ModelTokensChangedOutgoingEvent)
}

///|
/// Merges only another cursor event. Earliest old state/version remain; latest
/// new state/version/source/reason win.
fn CursorStateChangedEvent::attempt_to_merge(
  self : CursorStateChangedEvent,
  other : PendingOutgoingEvent,
) -> CursorStateChangedEvent? {
  match other {
    ViewZonesEvent(_) => None
    HiddenAreasEvent(_) => None
    ModelTokensEvent(_) => None
    CursorEvent(other) =>
      Some({
        kind: CursorStateChanged,
        old_selections: self.old_selections,
        selections: other.selections,
        old_model_version_id: self.old_model_version_id,
        model_version_id: other.model_version_id,
        source: other.source,
        reason: other.reason,
        reached_max_cursor_count: self.reached_max_cursor_count ||
        other.reached_max_cursor_count,
      })
  }
}

///|
/// Merges only another ViewZones event and retains the first queued instance.
fn ViewZonesChangedEvent::attempt_to_merge(
  self : ViewZonesChangedEvent,
  other : PendingOutgoingEvent,
) -> ViewZonesChangedEvent? {
  match other {
    CursorEvent(_) => None
    HiddenAreasEvent(_) => None
    ModelTokensEvent(_) => None
    ViewZonesEvent(_) => Some(self)
  }
}

///|
/// Merges only another hidden-area event and retains the first queued instance.
fn HiddenAreasChangedOutgoingEvent::attempt_to_merge(
  self : HiddenAreasChangedOutgoingEvent,
  other : PendingOutgoingEvent,
) -> HiddenAreasChangedOutgoingEvent? {
  match other {
    CursorEvent(_) | ViewZonesEvent(_) | ModelTokensEvent(_) => None
    HiddenAreasEvent(_) => Some(self)
  }
}

///|
/// Unlike cursor, ViewZones, and hidden-area siblings, the source model-token
/// wrapper never merges, even with another model-token event
/// (`viewModelEventDispatcher.ts:556-559`).
fn ModelTokensChangedOutgoingEvent::attempt_to_merge(
  _self : ModelTokensChangedOutgoingEvent,
  _other : PendingOutgoingEvent,
) -> ModelTokensChangedOutgoingEvent? {
  None
}

///|
/// Whether one queued outgoing fact can be dropped before delivery.
fn PendingOutgoingEvent::is_no_op(self : PendingOutgoingEvent) -> Bool {
  match self {
    CursorEvent(event) => event.is_no_op()
    ViewZonesEvent(event) => event.is_no_op()
    HiddenAreasEvent(event) => event.is_no_op()
    ModelTokensEvent(event) => event.is_no_op()
  }
}

///|
/// Attempts a same-kind merge and preserves the existing queue slot.
fn PendingOutgoingEvent::attempt_to_merge(
  self : PendingOutgoingEvent,
  other : PendingOutgoingEvent,
) -> PendingOutgoingEvent? {
  match self {
    CursorEvent(event) =>
      event.attempt_to_merge(other).map(merged => CursorEvent(merged))
    ViewZonesEvent(event) =>
      event.attempt_to_merge(other).map(merged => ViewZonesEvent(merged))
    HiddenAreasEvent(event) =>
      event.attempt_to_merge(other).map(merged => HiddenAreasEvent(merged))
    ModelTokensEvent(event) =>
      event.attempt_to_merge(other).map(merged => ModelTokensEvent(merged))
  }
}

///|
/// DOM-free outgoing-only half of Monaco's `ViewModelEventDispatcher`.
/// Pending cursor/ViewZones/hidden-area events coalesce into their first
/// same-kind slot; delivery is FIFO, discards no-ops, and remains safe when
/// listeners emit reentrantly.
struct CursorEventDispatcher {
  event_listeners : Array[OutgoingEventListenerCell]
  outgoing_events : Array[PendingOutgoingEvent]
  mut listener_count : Int32
  mut listeners_are_array : Bool
  mut delivery_index : Int32
  mut delivery_end : Int32
  mut delivery_value : PendingOutgoingEvent?
  mut delivery_active : Bool
  mut disposed : Bool
}

///|
/// Stable listener slot. Disposals mark slots inactive instead of changing the
/// array while a source-shaped delivery walk is in progress.
priv struct OutgoingEventListenerCell {
  listener : (PendingOutgoingEvent) -> Unit
  active : Ref[Bool]
}

///|
/// Creates an empty dispatcher.
fn CursorEventDispatcher::CursorEventDispatcher() -> CursorEventDispatcher {
  {
    event_listeners: [],
    outgoing_events: [],
    listener_count: 0,
    listeners_are_array: false,
    delivery_index: -1,
    delivery_end: 0,
    delivery_value: None,
    delivery_active: false,
    disposed: false,
  }
}

///|
/// Retires the registered outgoing listeners and drops any queued facts.
/// Later subscriptions and emissions are inert, matching the source-owned
/// emitter's registration in the ViewModel lifetime store.
fn CursorEventDispatcher::dispose(self : CursorEventDispatcher) -> Unit {
  if self.disposed {
    return
  }
  self.disposed = true
  self.outgoing_events.clear()
  for cell in self.event_listeners {
    cell.active.val = false
  }
  self.event_listeners.clear()
  self.listener_count = 0
  self.reset_event_delivery()
}

///|
/// Registers one listener on the shared heterogeneous outgoing emitter.
fn CursorEventDispatcher::on_outgoing_event(
  self : CursorEventDispatcher,
  listener : (PendingOutgoingEvent) -> Unit,
) -> @base_common.Disposable {
  if self.disposed {
    return @base_common.Disposable::from(() => ())
  }
  if self.listener_count == 1 && !self.listeners_are_array {
    // Monaco upgrades from its optimized single-listener representation to a
    // sparse array when the second listener is registered. It deliberately
    // does not downgrade that array while one listener remains.
    self.listeners_are_array = true
  }
  let active = Ref(true)
  self.event_listeners.push({ listener, active, })
  self.listener_count += 1
  @base_common.Disposable::from(() => {
    if active.val {
      active.val = false
      self.listener_count -= 1
      if self.listener_count == 0 {
        self.listeners_are_array = false
      }
    }
  })
}

///|
/// Exposes the dispatcher's cursor event through a typed listener alias.
fn CursorEventDispatcher::on_event(
  self : CursorEventDispatcher,
  listener : (CursorStateChangedEvent) -> Unit,
) -> @base_common.Disposable {
  self.on_outgoing_event(event => {
    if event is CursorEvent(event) {
      listener(event)
    }
  })
}

///|
/// Exposes the dispatcher's ViewZones event through a typed listener alias.
fn CursorEventDispatcher::on_view_zones_changed(
  self : CursorEventDispatcher,
  listener : (ViewZonesChangedEvent) -> Unit,
) -> @base_common.Disposable {
  self.on_outgoing_event(event => {
    if event is ViewZonesEvent(event) {
      listener(event)
    }
  })
}

///|
/// Exposes the dispatcher's hidden-area event through a typed listener alias.
fn CursorEventDispatcher::on_hidden_areas_changed(
  self : CursorEventDispatcher,
  listener : (HiddenAreasChangedOutgoingEvent) -> Unit,
) -> @base_common.Disposable {
  self.on_outgoing_event(event => {
    if event is HiddenAreasEvent(event) {
      listener(event)
    }
  })
}

///|
/// Exposes the model-token outgoing arm through a typed ViewModel listener.
fn CursorEventDispatcher::on_model_tokens_changed(
  self : CursorEventDispatcher,
  listener : (ModelTokensChangedOutgoingEvent) -> Unit,
) -> @base_common.Disposable {
  self.on_outgoing_event(event => {
    if event is ModelTokensEvent(event) {
      listener(event)
    }
  })
}

///|
/// Adds or merges first, then attempts an immediate drain.
fn CursorEventDispatcher::emit_outgoing_event(
  self : CursorEventDispatcher,
  event : CursorStateChangedEvent,
) -> Unit {
  if self.disposed {
    return
  }
  self.add_outgoing_event(CursorEvent(event))
  self.emit_outgoing_events()
}

///|
/// Adds a ViewZones fact to the same heterogeneous queue and drains it.
fn CursorEventDispatcher::emit_view_zones_changed(
  self : CursorEventDispatcher,
  event : ViewZonesChangedEvent,
) -> Unit {
  if self.disposed {
    return
  }
  self.add_outgoing_event(ViewZonesEvent(event))
  self.emit_outgoing_events()
}

///|
/// Adds a hidden-area fact to the shared heterogeneous queue and drains it.
fn CursorEventDispatcher::emit_hidden_areas_changed(
  self : CursorEventDispatcher,
  event : HiddenAreasChangedOutgoingEvent,
) -> Unit {
  if self.disposed {
    return
  }
  self.add_outgoing_event(HiddenAreasEvent(event))
  self.emit_outgoing_events()
}

///|
/// Adds one nonmergeable model-token fact to the shared outgoing queue.
fn CursorEventDispatcher::emit_model_tokens_changed(
  self : CursorEventDispatcher,
  event : ModelTokensChangedOutgoingEvent,
) -> Unit {
  if self.disposed {
    return
  }
  self.add_outgoing_event(ModelTokensEvent(event))
  self.emit_outgoing_events()
}

///|
/// Scans queued events in order and replaces the first mergeable slot;
/// otherwise appends at the FIFO tail.
fn CursorEventDispatcher::add_outgoing_event(
  self : CursorEventDispatcher,
  event : PendingOutgoingEvent,
) -> Unit {
  if self.disposed {
    return
  }
  for index, pending in self.outgoing_events {
    if pending.attempt_to_merge(event) is Some(merged) {
      self.outgoing_events[index] = merged
      return
    }
  }
  self.outgoing_events.push(event)
}

///|
/// Drains FIFO until empty. There is deliberately no local reentrancy gate:
/// the source recursively shifts/fires a reentrant outgoing event immediately
/// unless its separate generic view-event collector is active (that collector
/// is outside this child). The root Viewer's delivery FIFO, not this semantic
/// dispatcher, keeps public position/selection pairs adjacent.
fn CursorEventDispatcher::emit_outgoing_events(
  self : CursorEventDispatcher,
) -> Unit {
  if self.disposed {
    return
  }
  while self.outgoing_events.length() > 0 {
    let event = self.outgoing_events.remove(0)
    if !event.is_no_op() {
      self.deliver_event(event)
    }
  }
}

///|
/// Resets the listener-level delivery cursor. Incrementing the cursor before a
/// callback and finishing an active walk at the start of a nested `fire` are
/// the two ordering rules owned by Monaco's `Emitter._deliverQueue`/`fire`.
fn CursorEventDispatcher::reset_event_delivery(
  self : CursorEventDispatcher,
) -> Unit {
  self.delivery_index = self.delivery_end
  self.delivery_end = 0
  self.delivery_value = None
  self.delivery_active = false
}

///|
/// Finishes the currently enqueued listener walk. A nested fire calls this
/// before enqueuing its own value, so the remaining listeners observe A, then
/// all listeners observe B, before the first A callback resumes.
fn CursorEventDispatcher::finish_event_delivery(
  self : CursorEventDispatcher,
) -> Unit {
  while self.delivery_active &&
        self.delivery_index < self.delivery_end &&
        !self.disposed {
    let index = self.delivery_index
    // Source increments before invoking the listener because the callback can
    // recursively finish this same delivery state.
    self.delivery_index += 1
    let cell = self.event_listeners[index]
    if cell.active.val {
      guard self.delivery_value is Some(value) else {
        self.reset_event_delivery()
        return
      }
      (cell.listener)(value)
    }
  }
  if self.delivery_active &&
    (self.delivery_index >= self.delivery_end || self.disposed) {
    self.reset_event_delivery()
  }
}

///|
/// Mirrors the source emitter's direct single-listener representation and its
/// listener-level `EventDeliveryQueue`. This cannot be an outer event FIFO:
/// on a nested fire, the source first drains the remaining listeners for the
/// current value and delivers the nested value before returning to the caller.
fn CursorEventDispatcher::deliver_event(
  self : CursorEventDispatcher,
  event : PendingOutgoingEvent,
) -> Unit {
  if self.disposed {
    return
  }
  if self.delivery_active {
    self.finish_event_delivery()
  }
  if self.disposed || self.listener_count == 0 {
    return
  }
  if !self.listeners_are_array {
    for cell in self.event_listeners {
      if cell.active.val {
        (cell.listener)(event)
        return
      }
    }
    return
  }
  self.delivery_index = 0
  self.delivery_end = self.event_listeners.length()
  self.delivery_value = Some(event)
  self.delivery_active = true
  self.finish_event_delivery()
}

///|
pub extend CursorStateChangedEvent with Debug::{to_repr}

///|
pub extend HiddenAreasChangedOutgoingEvent with Debug::{to_repr}

///|
pub extend OutgoingViewModelEventKind with Debug::{to_repr}

///|
pub extend OutgoingViewModelEventKind with Eq::{equal, not_equal}

///|
pub extend ViewZonesChangedEvent with Debug::{to_repr}