import { on } from './on.mjs';
import { once } from './once.mjs';
import { addEventHandler } from './handler.mjs';

/**
 * Internal events triggered by an `EventEmitter` as handlers are
 * added and removed.
 */

/**
 * An object that can emit events. This is a minimal alternative to the `EventTarget`
 * interface available in most JavaScript environments. Unlike the callback functions
 * you provide to `EventTarget.addEventListener()`, `EventEmitter` does not support
 * event propagation, but does provide convenient `once()` and `on()` methods for
 * converting event listeners into into `Promise`s and `AsyncGenerator`s.
 */
class EventEmitter {
  /**
   * A map containing the event handlers registered for events on this
   * emitter. You should not directly mutate this map, but you can use it
   * to introspect the number of handlers currently registered.
   */
  all = (() => new Map())();

  /**
   * A reference to an `EventTarget` that is being wrapped with this `EventEmitter`.
   * As handlers are added for events on the emitter, matching events are listened
   * for lazily on the `EventTarget`. This is useful for converting event producers
   * in existing environments into objects that can be used with `once()` and `on()`.
   *
   * @example
   * const button = document.querySelector('button');
   * const emitter = new EventEmitter(button);
   * // emitter.eventTarget === button
   *
   * const click = await emitter.once('click');
   * console.log('clicked!', click);
   */

  /**
   * An `EventEmitter` that triggers events when handlers are added
   * or removed from this emitter. This is useful for debugging, and for
   * building higher-level abstractions that need to know when handlers
   * are registered for a given event.
   */
  get internal() {
    if (this._internal) {
      return this._internal;
    } else {
      const internal = new EventEmitter();
      this._internal = internal;
      return internal;
    }
  }
  handleEvent = (event, handler, options) => {
    const signal = options?.signal;
    const once = options?.once;
    let handlers = this.all.get(event);
    const signalAbort = signal && new AbortController();
    const remove = () => {
      signalAbort?.abort();
      if (handlers == null) return;
      this._internal?.emit('remove', {
        event,
        all: handlers,
        handler
      });
      handlers.delete(handler);
      if (handlers.size === 0) this.all.delete(event);
    };
    const normalizedHandler = once ? (...args) => {
      remove();
      handler(...args);
    } : handler;
    if (handlers) {
      handlers.add(normalizedHandler);
    } else {
      handlers = new Set();
      handlers.add(normalizedHandler);
      this.all.set(event, handlers);
    }
    this._internal?.emit('add', {
      event,
      all: handlers,
      handler
    });
    signal?.addEventListener('abort', remove, {
      signal: signalAbort.signal
    });
  };
  constructor(eventTarget) {
    this.on = this.on.bind(this);
    this.once = this.once.bind(this);
    this.emit = this.emit.bind(this);
    if (eventTarget) {
      this.eventTarget = eventTarget;
      const abortMap = new Map();
      this.internal.on('add', ({
        event: eventName,
        all
      }) => {
        if (all.size !== 1) return;
        const abort = new AbortController();
        abortMap.set(eventName, abort);
        addEventHandler(eventTarget, eventName, event => {
          this.emit(eventName, event);
        }, {
          signal: abort.signal
        });
      });
      this.internal.on('remove', ({
        event: eventName,
        all
      }) => {
        const abort = all.size === 0 ? abortMap.get(eventName) : undefined;
        if (abort == null) return;
        abort.abort();
        abortMap.delete(eventName);
      });
    }
  }

  /**
   * Listens to an `event` on the `target` object. Instead of providing a callback
   * function to run when the event is emitted, this function returns an `AsyncGenerator`
   * that yields each event as it is emitted.
   *
   * @param event The name of the event you are targeting.
   *
   * @returns An `AsyncGenerator` that yields each event as it is emitted.
   *
   * @example
   * const emitter = new EventEmitter<{message: string}>();
   *
   * for await (const message of emitter.on('message')) {
   *   console.log('message', message);
   * }
   */

  /**
   * Listens for `event` to be triggered.
   *
   * @param event The name of the event you are targeting.
   * @param handler The function to call when the event is triggered.
   *
   * @example
   * const emitter = new EventEmitter<{message: string}>();
   *
   * emitter.on('message', (message) => {
   *   console.log('message', message);
   * });
   */

  on(event, argOne, argTwo) {
    return typeof argOne === 'function' ? this.handleEvent(event, argOne, argTwo) : on(this.handleEvent, event, argOne);
  }

  /**
   * Listens for the next matching `event` to be triggered.
   *
   * @param event The name of the event you are targeting.
   *
   * @returns A `Promise` that resolves when the next matching event is triggered.
   * The promise resolves with the value of the first argument passed to the event.
   * If you pass the `signal` option, the promise may also resolve with `undefined`
   * (in case the signal is aborted before the event is emitted). If you also set
   * the `abort` option to `'reject'`, the promise will instead reject with an
   * `AbortError` when the signal is aborted.
   *
   * @example
   * const emitter = new EventEmitter<{message: string}>();
   *
   * const nextMessage = await emitter.once('message');
   * console.log('message', nextMessage);
   */

  /**
   * Listens for the next matching `event` to be triggered.
   *
   * @param event The name of the event you are targeting.
   * @param handler The function to call when the event is triggered.
   *
   * @example
   * const emitter = new EventEmitter<{message: string}>();
   *
   * emitter.once('message', (message) => {
   *   console.log('message', message);
   * });
   */

  once(event, argOne, argTwo) {
    return typeof argOne === 'function' ? this.handleEvent(event, argOne, {
      ...argTwo,
      once: true
    }) : once(this.handleEvent, event, argOne);
  }

  /**
   * Emits an event with the given `data`. This will trigger all handlers
   * listening for the provided `event`.
   */
  emit(event, data) {
    const handlers = this.all.get(event);
    if (handlers) {
      for (const handler of Array.from(handlers)) {
        handler(data);
      }
    }
  }
}

/**
 * Creates an object that can emit events. This is a minimal alternative to the `EventTarget`
 * interface available in most JavaScript environments. Unlike the callback functions
 * you provide to `EventTarget.addEventListener()`, `EventEmitter` does not support
 * event propagation, but does provide convenient `once()` and `on()` methods for
 * converting event listeners into into `Promise`s and `AsyncGenerator`s.
 */
function createEventEmitter() {
  return new EventEmitter();
}

export { EventEmitter, createEventEmitter };
