/*
 * EventEmiter
 * @class
 *
 * This class mimicks the nodejs EventEmitter. It is primarily used as the
 * global eventbus so different parts of the application can communicate with
 * each other without being tightly coupled.
 * e.g. store -> http client, component -> http client, component -> socket etc.
 */

type Listener = (...args: any[]) => void
type BroadcastHook = (eventName: string, data: unknown) => void

class EventEmitter {
  /*
   * @type {object} events - container to hold key value pairs of events/functions
   */
  private events: Record<string, Listener[]>

  /*
   * @type {object} eventMap - map of available events
   * For the default eventbus see ./lib/eventMap.ts
   */
  private eventMap?: Record<string, any>

  /*
   * @type {function} broadcastHook - optional hook for broadcasting plugin events
   */
  private broadcastHook?: BroadcastHook

  /*
   * @param {array} eventMap
   */
  constructor(eventMap?: Record<string, any>) {
    /*
     * @type {object} events - container to hold key value pairs of events/functions
     */
    this.events = {}
    /*
     * @type {object} eventMap - map of available events
     */
    this.eventMap = eventMap
  }

  /*
   * setBroadcastHook
   *
   * Registers a hook function that will be called for plugin events (events containing "--")
   * This allows the EventEmitter to broadcast plugin events to sockets without direct dependencies
   *
   * @param {function} hook - callback function that receives (eventName, data)
   */
  setBroadcastHook(hook: BroadcastHook): void {
    this.broadcastHook = hook
  }

  /*
   * on
   *
   * @param {string} event - event name
   * @param {function} listener - callback function
   */
  // eslint-disable-next-line id-length
  on(event: string, listener: Listener): () => void {
    if (!this.isValid(event)) {
      return () => {}
    }
    if (typeof this.events[event] !== "object") {
      this.events[event] = []
    }
    this.events[event].push(listener)

    return () => this.removeListener(event, listener)
  }

  /*
   * removeListener
   *
   * @param {string} event - event name
   * @param {function} listener - callback function
   */
  removeListener(event: string, listener: Listener): void {
    if (typeof this.events[event] === "object") {
      const idx = this.events[event].indexOf(listener)

      if (idx > -1) {
        this.events[event].splice(idx, 1)
      }
    }
  }

  /*
   * emit
   *
   * @param {string} event - event name
   * @param {string|object|boolean} - arbitrary arguments
   */
  emit(event: string, ...args: any[]): boolean {
    if (!this.isValid(event)) {
      return false
    }

    const hasListeners = typeof this.events[event] === "object"
    if (hasListeners) {
      this.events[event].forEach((listener) => listener.apply(this, args))
    }

    // Broadcast plugin events (those containing "--") to sockets if hook is registered
    // Plugin events have format: "pluginKey--eventName" (e.g., "pacifico__cheesy-slides--marpUpdated")
    // We broadcast regardless of whether there are listeners, as this is for socket communication
    if (this.broadcastHook && event.includes("--") && args.length > 0) {
      this.broadcastHook(event, args[0])
    }

    return hasListeners
  }

  /*
   * once
   *
   * @param {string} event - event name
   * @param {function} listener - callback function
   */
  once(event: string, listener: Listener): void {
    if (!this.isValid(event)) {
      return
    }
    const remove = this.on(event, (...args) => {
      remove()
      listener.apply(this, args)
    })
  }

  /*
   * isValid
   *
   * If an eventMap is present this method checks if the given event exists as
   * key in the map. Plugin events (containing "--") skip eventMap validation.
   * @param {string} event - event name
   */
  private isValid(event: string): boolean {
    // Skip eventMap validation for plugin events (containing "--")
    // Plugin events have format: "pluginKey--eventName"
    if (event.includes("--")) {
      return true
    }

    // early return if we don't have an eventMap
    if (!this.eventMap) {
      return true
    }

    // throw error if the event is unknown
    if (!this.eventMap.hasOwnProperty(event)) {
      throw new Error(
        `Unkown event passed to eventbus, please check or update: "${event}"`,
      )
    }

    // return true if the event exists in the eventMap
    return true
  }
}

export default EventEmitter
export type { Listener }
