import { EventEmitter } from 'events';
import { AsyncResource } from 'async_hooks';

const kEventEmitter = Symbol('kEventEmitter');
const kAsyncResource = Symbol('kAsyncResource');

type EventEmitterOptions = typeof EventEmitter extends {
  new (options?: infer T) : EventEmitter;
} ? T : never;

type AsyncResourceOptions = typeof AsyncResource extends {
  new (name : string, options?: infer T) : AsyncResource;
} ? T : never;

type Options = EventEmitterOptions & AsyncResourceOptions & {
  name?: string;
};

class EventEmitterReferencingAsyncResource extends AsyncResource {
  [kEventEmitter] : EventEmitter;

  constructor (ee: EventEmitter, type: string, options?: AsyncResourceOptions) {
    super(type, options);
    this[kEventEmitter] = ee;
  }

  get eventEmitter () : EventEmitter {
    return this[kEventEmitter];
  }
}

class EventEmitterAsyncResource extends EventEmitter {
  [kAsyncResource] : EventEmitterReferencingAsyncResource;

  constructor (options?: Options | string) {
    let name;
    if (typeof options === 'string') {
      name = options;
      options = undefined;
    } else {
      name = options?.name || new.target.name;
    }
    super(options);

    this[kAsyncResource] =
      new EventEmitterReferencingAsyncResource(this, name, options);
  }

  emit (event: string | symbol, ...args: any[]) : boolean {
    return this.asyncResource.runInAsyncScope(
      super.emit, this, event, ...args);
  }

  emitDestroy () : void {
    this.asyncResource.emitDestroy();
  }

  asyncId () : number {
    return this.asyncResource.asyncId();
  }

  triggerAsyncId () : number {
    return this.asyncResource.triggerAsyncId();
  }

  get asyncResource () : EventEmitterReferencingAsyncResource {
    return this[kAsyncResource];
  }

  static get EventEmitterAsyncResource () { return EventEmitterAsyncResource; }
}

export = EventEmitterAsyncResource;
