type EventMap = Record<string, any>;

type EventKey<T extends EventMap> = string & keyof T;
type EventReceiver<T> = (params: T) => void;

interface Emitter<T extends EventMap> {
  on<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): void;
  off<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): void;
  emit<K extends EventKey<T>>(eventName: K, data: T[K]): void;
  once<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): void;
}

class EventEmitter<T extends EventMap> implements Emitter<T> {
  private listeners: {
    [K in keyof T]?: Array<EventReceiver<T[K]>>;
  } = {};

  on<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): void {
    if (!this.listeners[eventName]) {
      this.listeners[eventName] = [];
    }
    this.listeners[eventName]!.push(fn);
  }

  off<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): void {
    const handlers = this.listeners[eventName];
    if (handlers) {
      this.listeners[eventName] = handlers.filter(f => f !== fn);
    }
  }

  emit<K extends EventKey<T>>(eventName: K, data: T[K]): void {
    const handlers = this.listeners[eventName];
    if (handlers) {
      handlers.forEach(fn => fn(data));
    }
  }

  once<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): void {
    const onceWrapper = ((data: T[K]) => {
      fn(data);
      this.off(eventName, onceWrapper);
    }) as EventReceiver<T[K]>;
    this.on(eventName, onceWrapper);
  }
}

export default EventEmitter;