import log from "@racletteCore/lib/logger"
const onceTimeoutMs = 5000
/**
 * 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.
 */

class EventEmitter {
  /**
   * @param {array} eventWhitelist
   * @param {array} eventWildcards
   */
  constructor(eventWhitelist, eventWildcards) {
    /**
     * @type {object} events - container to hold key valye pairs of events/functions
     */
    this.events = {}
    /**
     * @type {object} eventWhitelist - map of available events
     * For the default eventbus see ./lib/eventWhitelist.ts
     */
    this.eventWhitelist = eventWhitelist
    this.eventWildcards = eventWildcards
  }

  /**
   * on
   *
   * @param {string} event - event name
   * @param {function} listener - callback function
   */
  on(event, listener) {
    if (!this.isValid(event)) {
      return false
    }
    // log.event(`${event} on registered`)
    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, listener) {
    // log.event(`${event} listener removed`)
    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 arguements
   */
  emit(event, ...args) {
    if (!this.isValid(event)) {
      return false
    }
    log.event(`Emit sent for ${event}`, args)
    if (typeof this.events[event] === "object") {
      this.events[event].forEach((listener) => listener.apply(this, args))
    }
  }

  /**
   * once
   *
   * @param {string} event - event name
   * @param {string|object|boolean} - arbitrary arguements
   */
  once(event, listener): Function | boolean {
    if (!this.isValid(event)) {
      return false
    }
    let remove
    // Create a timeout to log an error if not triggered
    const timeoutId = setTimeout(() => {
      log.event(
        `Once listener for ${event} not triggered within ${onceTimeoutMs / 1000} second, will now be removed`,
      )
      remove()
      listener.apply(this, { error: "notTriggered" })
    }, onceTimeoutMs)

    log.event(`${event} once registered`)
    remove = this.on(event, (...args) => {
      log.event(`${event} once received`, args)
      // Clear the timeout when the event is received
      clearTimeout(timeoutId)
      remove()
      listener.apply(this, args)
    })
  }

  /**
   * isValid
   *
   * If an eventWhitelist is present this method checks if the given event exists as
   * key in the map.
   * @param {string} event - event name
   */
  isValid(event) {
    // early return if we don't have an eventWhitelist
    if (!this.eventWhitelist) {
      return true
    }
    if (this.eventWildcards) {
      let isValid = false

      this.eventWildcards.forEach((namePartial) => {
        if (event.indexOf(namePartial) > -1) {
          isValid = true
        }
      })
      if (isValid) {
        return true
      }
    }
    // throw error if the event is unknown
    if (!this.eventWhitelist.indexOf(event) < 0) {
      throw new Error(
        `Unkown event ${event}, please check or update ./lib/eventWhitelist.ts`,
      )
    }

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

export default EventEmitter
