interface EventSourceFactory {
  new (path: string): EventSource
}

export const injectEvents = (eventSource: EventSourceFactory) => {
  /**
   * EventSource wrapper
   *
   * @remarks
   * wraps EventSource in a function to allow for
   * mocking in tests
   */
  return class Events extends eventSource {
    /**
     * Singleton constructor
     *
     */
    public static make(
      options: {name: string; path: string} & Partial<Options>,
    ): Events {
      if (typeof window.bud.hmr[options.name] === `undefined`)
        Object.assign(window.bud.hmr, {
          [options.name]: new Events(options),
        })

      return window.bud.hmr[options.name]
    }
    /**
     * Registered listeners
     */
    public listeners: Set<Listener> = new Set<Listener>()

    /**
     * EventSource `onmessage` handler
     */
    public override onmessage = async function (payload: MessageEvent) {
      if (!payload?.data || payload.data == `\uD83D\uDC93`) {
        return
      }

      try {
        const data = JSON.parse(payload.data)
        if (!data) return

        await Promise.all(
          [...this.listeners].map(async listener => {
            return await listener(data)
          }),
        )
      } catch (ex) {}
    }

    /**
     * EventSource `onopen` handler
     */
    public override onopen = function () {}

    /**
     * Class constructor
     *
     * @remarks
     * Singleton interface, so this is private.
     *
     */
    private constructor(
      public options: {name: string; path: string} & Partial<Options>,
    ) {
      super(options.path)

      this.onopen = this.onopen.bind(this)
      this.onmessage = this.onmessage.bind(this)
      this.addListener = this.addListener.bind(this)
    }

    /**
     * EventSource `addMessageListener` handler
     */
    public addListener(listener: Listener): this {
      this.listeners.add(listener)
      return this
    }
  }
}
