/**
 * EventBus.js
 * Simple publish/subscribe event system for decoupled communication
 * between game systems.
 */

class EventBus {
  constructor() {
    /** @type {Map<string, Array<{fn: Function, once: boolean}>>} */
    this._listeners = new Map();
  }

  /**
   * Subscribe to an event.
   * @param {string} event
   * @param {Function} fn
   * @returns {Function} Unsubscribe function.
   */
  on(event, fn) {
    if (!this._listeners.has(event)) {
      this._listeners.set(event, []);
    }
    const entry = { fn, once: false };
    this._listeners.get(event).push(entry);
    return () => this.off(event, fn);
  }

  /**
   * Subscribe to an event, but only fire once.
   * @param {string} event
   * @param {Function} fn
   */
  once(event, fn) {
    if (!this._listeners.has(event)) {
      this._listeners.set(event, []);
    }
    this._listeners.get(event).push({ fn, once: true });
  }

  /**
   * Unsubscribe from an event.
   * @param {string} event
   * @param {Function} fn
   */
  off(event, fn) {
    const list = this._listeners.get(event);
    if (!list) return;
    const idx = list.findIndex((entry) => entry.fn === fn);
    if (idx !== -1) list.splice(idx, 1);
  }

  /**
   * Emit an event with optional data.
   * @param {string} event
   * @param {*} [data]
   */
  emit(event, data) {
    const list = this._listeners.get(event);
    if (!list || list.length === 0) return;

    // Iterate in reverse so we can safely remove "once" entries
    for (let i = list.length - 1; i >= 0; i--) {
      const entry = list[i];
      entry.fn(data);
      if (entry.once) {
        list.splice(i, 1);
      }
    }
  }

  /**
   * Remove all listeners for a specific event, or all events.
   * @param {string} [event]
   */
  clear(event) {
    if (event) {
      this._listeners.delete(event);
    } else {
      this._listeners.clear();
    }
  }
}

module.exports = EventBus;
