'use strict';

import LoggerManager from '../logger';

/**
 * Custom event emitter implementation for use
 */
class EventEmitter<L extends ListenerSignature<L> = DefaultListener> {

  private _listeners: {[E in keyof L]?: Set<L[E]>};
  private _eventEmitterLogger = LoggerManager.getLogger('EventEmitter');

  /**
   * Constructs instance
   */
  constructor() {
    this._listeners = {};
  }

  /**
   * Subscribes a listener to event
   * @param event Event to subscribe to
   * @param callback Listener function to subscribe
   */
  on<U extends EventEmitter.Event<L>>(event: U, callback: L[U]) {
    if (!event) {
      throw new Error('Event name is empty or undefined');
    }
    this._listeners[event] = this._listeners[event] || new Set();
    this._listeners[event].add(callback);
  }

  /**
   * Unsubscribes a listener from event
   * @param event Event to unsubscribe from
   * @param callback Callback to unsubscribe
   */
  off<U extends EventEmitter.Event<L>>(event: U, callback: L[U]) {
    if (this._listeners[event]) {
      this._listeners[event].delete(callback);
      if (!this._listeners[event].size) {
        delete this._listeners[event];
      }
    }
  }

  /**
   * Subscribes a listener to event once
   * @param event Event to subscribe to
   * @param callback Listener function to subscribe
   * @param options Additional options
   */
  once<U extends EventEmitter.Event<L>>(event: U, callback: L[U], options?: EventEmitter.OnceOptions<L[U]>) {
    let listener: any = ((...args: Parameters<L[U]>) => {
      if (!options?.ifArgs || options?.ifArgs(...args)) {
        this.off(event, listener);
        return callback(...args);
      }
    });
    this.on(event, listener);
  }

  /**
   * Emits an event
   * @param event Event to emit
   * @param data Event payload
   * @returns Promise resolving when all listeners calls completed. Usually async version is useful if listeners may
   * return promises and the call should wait for them. If one of listener rejects or throws an error, it will be
   * logged. Async listeners called all at once
   */
  async emit<U extends EventEmitter.Event<L>>(event: U, ...args: Parameters<L[U]>): Promise<void> {
    if (!this._listeners[event]?.size) {
      return;
    }
    let results = [];
    for (let listener of this._listeners[event]) {
      results.push(this._callListener(event as string, listener, args));
    }
    return Promise.all(results) as any;
  }

  /**
   * Returns subscribed events
   * @returns subscribed events
   */
  getSubscriptions(): EventEmitter.Event<L>[] {
    return Object.keys(this._listeners) as EventEmitter.Event<L>[];
  }

  /**
   * Returns subscribed event listeners
   * @param event Event
   * @returns Listeners
   */
  getListeners<U extends EventEmitter.Event<L>>(event: U): L[U][] {
    if (!this._listeners[event]?.size) {
      return [];
    }
    return [...this._listeners[event].values()];
  }

  /**
   * Returns whether has listeners on specific event
   * @param event Event
   * @returns Whether has listeners
   */
  hasListeners<U extends EventEmitter.Event<L>>(event: U): boolean {
    return !!this._listeners[event];
  }

  private async _callListener(event: string, listener: (...args: any) => any, args: any[]) {
    try {
      await listener(...args);
    } catch (err) {
      this._eventEmitterLogger.error(`${event}: listener failed with error`, err);
    }
  }
}

namespace EventEmitter {

  /** Method options */
  export type OnceOptions<Listener extends (...args: any[]) => any> = {
    /**
     * If specified, the listener will be called only if the callback returns true. Otherwise, the listener will wait
     * for the next matching call
     */
    ifArgs?: (...args: Parameters<Listener>) => boolean;
  };

  /** Infers event type of a listener */
  export type Event<L extends ListenerSignature<L>> = Extract<keyof L, string>;
}

export default EventEmitter;

/**
 * Event emitter listener type
 */
export type ListenerSignature<Listeners> = {
  [EventName in keyof Listeners]: (...args: any[]) => any;
};

/**
 * Default event emitter listener type
 */
export type DefaultListener = {
  [k: string]: (...args: any[]) => any;
};
