import { addEventHandler } from './handler.mjs';
import { AbortError } from './abort/AbortError.mjs';
import { NestedAbortController } from './abort/NestedAbortController.mjs';

/**
 * Listens for the next `event` on the `target` object. Instead of providing a callback
 * function to run when the event is emitted, this function returns a `Promise`
 * that resolves when the next matching event is triggered.
 *
 * @param target The target to add the event handler to.
 * @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 button = document.querySelector('button');
 *
 * const nextClickEvent = await once(button, 'click');
 * console.log('clicked!', nextClickEvent);
 */

async function once(target, event, options) {
  const signal = options?.signal;
  const abortBehavior = options?.abort ?? 'return';
  if (signal?.aborted) {
    if (abortBehavior === 'return') {
      return undefined;
    } else {
      throw new AbortError();
    }
  }
  const listenerAbortController = signal ? new NestedAbortController(signal) : new AbortController();
  const signalAbortController = signal && new AbortController();
  return new Promise((resolve, reject) => {
    const resolver = (...args) => {
      signalAbortController?.abort();
      resolve(args.length > 1 ? args : args[0]);
    };
    addEventHandler(target, event, resolver, {
      once: true,
      signal: listenerAbortController.signal
    });
    if (signal) {
      addEventHandler(signal, 'abort', () => {
        signalAbortController.abort();
        if (abortBehavior === 'return') {
          resolve();
        } else {
          reject(new AbortError());
        }
      }, {
        once: true
      });
    }
  });
}

export { once };
