type Listener = {
    callback: (...args: unknown[]) => void;
    context?: unknown;
};

export class Emitter {
    private _events: Record<string, Listener[]> = {};

    on(name: string, callback: (...args: unknown[]) => void, context?: unknown): this {
        if (!this._events[name]) {
            this._events[name] = [];
        }
        this._events[name].push({ callback, context });
        return this;
    }

    once(name: string, callback: (...args: unknown[]) => void, context?: unknown): this {
        const onceWrapper = (...args: unknown[]) => {
            this.off(name, onceWrapper);
            callback.apply(context, args);
        };
        return this.on(name, onceWrapper, context);
    }

    off(name?: string, callback?: (...args: unknown[]) => void): this {
        if (!name) {
            this._events = {};
            return this;
        }
        if (!this._events[name]) return this;
        if (!callback) {
            delete this._events[name];
            return this;
        }
        this._events[name] = this._events[name].filter(
            listener => listener.callback !== callback
        );
        return this;
    }

    removeAllListeners(): this {
        this._events = {};
        return this;
    }

    emit(name: string, ...args: unknown[]): this {
        if (!this._events[name]) return this;
        this._events[name].forEach(listener => {
            listener.callback.apply(listener.context, args);
        });
        return this;
    }

    listenerCount(name: string): number {
        return this._events[name]?.length ?? 0;
    }
}

export class EventBus extends Emitter { }

export const eventBus = new EventBus();

export const on = (eventName: string, callback: (...args: unknown[]) => void, context?: unknown): Emitter => {
    return eventBus.on(eventName, callback, context);
};

export const off = (eventName?: string, callback?: (...args: unknown[]) => void): Emitter => {
    return eventBus.off(eventName, callback);
};

export const once = (eventName: string, callback: (...args: unknown[]) => void, context?: unknown): Emitter => {
    return eventBus.once(eventName, callback, context);
};

export const emit = (eventName: string, ...args: unknown[]): Emitter => {
    return eventBus.emit(eventName, ...args);
};

export const removeAllListeners = (eventName?: string): Emitter => {
    if (eventName) return eventBus.off(eventName);
    return eventBus.removeAllListeners();
};
