/**
 * A representation of an `AbortSignal` that can be serialized between
 * two threads.
 */

/**
 * Converts a serialized `AbortSignal` into a “live” one, which you can
 * use to cancel operations in the current environment. When the signal aborts,
 * all memory associated with the signal will be released automatically.
 */
class ThreadAbortSignal {
  #abortController;
  #abortSignal;
  #onabort = null;

  // Proxy properties
  get aborted() {
    return this.#abortSignal.aborted;
  }
  get reason() {
    return this.#abortSignal.reason;
  }
  get onabort() {
    return this.#onabort;
  }
  set onabort(value) {
    if (this.#onabort) {
      this.#abortSignal.removeEventListener('abort', this.#onabort);
    }
    this.#onabort = value;
    if (value) {
      this.#abortSignal.addEventListener('abort', value);
    }
  }
  constructor(signal, {
    signal: killswitchSignal,
    retain,
    release
  } = {}) {
    if (isAbortSignal(signal)) {
      this.#abortSignal = signal;
    } else {
      this.#abortController = new AbortController();
      this.#abortSignal = this.#abortController.signal;
      const {
        aborted,
        start
      } = signal ?? {};
      if (aborted) {
        this.#abortController.abort();
      } else if (start) {
        retain?.(start);
        start(aborted => {
          if (aborted) this.#abortController.abort();
        });
        if (release) {
          killswitchSignal?.addEventListener('abort', () => () => release(start), {
            once: true,
            signal: this.#abortSignal
          });
          this.#abortSignal.addEventListener('abort', () => release(start), {
            once: true,
            signal: killswitchSignal
          });
        }
      }
    }
  }

  // Proxy methods
  addEventListener(...args) {
    return this.#abortSignal.addEventListener(...args);
  }
  removeEventListener(...args) {
    return this.#abortSignal.removeEventListener(...args);
  }
  dispatchEvent(...args) {
    return this.#abortSignal.dispatchEvent(...args);
  }
  throwIfAborted() {
    return this.#abortSignal.throwIfAborted();
  }

  /**
   * Converts an `AbortSignal` into a version of that signal that can
   * be transferred to a target `Thread`. The resulting object can be
   * serialized using the RPC utilities provided in this library, and
   * passed to `new ThreadAbortSignal()` to be converted into a “live”
   * `AbortSignal`.
   */
  static serialize(signal, {
    retain,
    release
  } = {}) {
    if (signal.aborted) {
      return {
        aborted: true
      };
    }
    const listeners = new Set();
    signal.addEventListener('abort', () => {
      for (const listener of listeners) {
        listener(signal.aborted);
        release?.(listener);
      }
      listeners.clear();
    }, {
      once: true
    });
    return {
      aborted: false,
      start(listener) {
        if (signal.aborted) {
          listener(true);
        } else {
          retain?.(listener);
          listeners.add(listener);
        }
      }
    };
  }

  /**
   * Checks if a value is a serialized ThreadSignal.
   */
  static isSerialized(value) {
    return typeof value === 'object' && value != null && typeof value.aborted === 'boolean' && typeof value.start === 'function';
  }
}
function isAbortSignal(value) {
  return value != null && typeof value.aborted === 'boolean' && typeof value.addEventListener === 'function';
}

export { ThreadAbortSignal };
