"use strict";
/** based on emittery npm package, which is MIT */
Object.defineProperty(exports, "__esModule", { value: true });
const anyMap = new WeakMap();
const eventsMap = new WeakMap();
const triggeredMap = new WeakMap();
const resolvedPromise = Promise.resolve();
//type EmitterCallback = (data?: any) => any;
function assertEventName(eventName) {
    if (typeof eventName !== 'string') {
        throw new TypeError('eventName must be a string');
    }
}
function assertListener(listener) {
    if (typeof listener !== 'function') {
        throw new TypeError('listener must be a function');
    }
}
function getListeners(instance, eventName) {
    const events = eventsMap.get(instance);
    if (!events) {
        // we're hitting this case only when calling `off` after the instance has been destroyed (so extremely rarely)
        // so we want to return an empty set in order not to make the code throw an error
        return new Set();
    }
    if (!events.has(eventName)) {
        events.set(eventName, new Set());
    }
    return events.get(eventName);
}
function defaultMethodNamesOrAssert(methodNames) {
    if (methodNames === undefined) {
        return allEmitteryMethods;
    }
    if (!Array.isArray(methodNames)) {
        throw new TypeError('`methodNames` must be an array of strings');
    }
    for (const methodName of methodNames) {
        if (!allEmitteryMethods.includes(methodName)) {
            if (typeof methodName !== 'string') {
                throw new TypeError('`methodNames` element must be a string');
            }
            throw new Error(`${methodName} is not Emittery method`);
        }
    }
    return methodNames;
}
class Emittery {
    static mixin(emitteryPropertyName, methodNames) {
        methodNames = defaultMethodNamesOrAssert(methodNames);
        return (target) => {
            if (typeof target !== 'function') {
                throw new TypeError('`target` must be function');
            }
            for (const methodName of methodNames) {
                if (target.prototype[methodName] !== undefined) {
                    throw new Error(`The property \`${methodName}\` already exists on \`target\``);
                }
            }
            function getEmitteryProperty() {
                Object.defineProperty(this, emitteryPropertyName, {
                    enumerable: false,
                    value: new Emittery(),
                });
                return this[emitteryPropertyName];
            }
            Object.defineProperty(target.prototype, emitteryPropertyName, {
                enumerable: false,
                get: getEmitteryProperty,
            });
            const emitteryMethodCaller = (methodName) => (function(...args) {
                return this[emitteryPropertyName][methodName](...args);
            });
            for (const methodName of methodNames) {
                Object.defineProperty(target.prototype, methodName, {
                    enumerable: false,
                    value: emitteryMethodCaller(methodName),
                });
            }
            return target;
        };
    }
    constructor() {
        anyMap.set(this, new Set());
        eventsMap.set(this, new Map());
        triggeredMap.set(this, new Map());
    }
    destroy() {
        this.clearListeners();
        // should not be needed, since those maps are weak
        // but let's do it anyway - some of our users suggested this Emitter class
        // could be a source of memory leaks, so let's clean this up
        anyMap.delete(this);
        eventsMap.delete(this);
        triggeredMap.delete(this);
    }
    on(eventName, listener) {
        assertEventName(eventName);
        assertListener(listener);
        getListeners(this, eventName).add(listener);
        return this.off.bind(this, eventName, listener);
    }
    off(eventName, listener) {
        assertEventName(eventName);
        assertListener(listener);
        getListeners(this, eventName).delete(listener);
    }
    once(eventName) {
        return new Promise((resolve) => {
            assertEventName(eventName);
            const off = this.on(eventName, (data) => {
                off();
                resolve(data);
            });
        });
    }
    onIncludeFiredOnce(eventName) {
        const triggeredEventsMap = triggeredMap.get(this);
        const eventInfo = triggeredEventsMap.get(eventName);
        if (eventInfo) {
            return Promise.resolve(eventInfo.data);
        }
        return this.once(eventName);
    }
    onIncludeFired(eventName, listener) {
        const triggeredEventsMap = triggeredMap.get(this);
        const eventInfo = triggeredEventsMap.get(eventName);
        if (eventInfo) {
            listener(eventInfo.data);
        }
        return this.on(eventName, listener);
    }
    async emit(eventName, eventData) {
        assertEventName(eventName);
        const listeners = getListeners(this, eventName);
        const triggeredEventsMap = triggeredMap.get(this);
        const anyListeners = anyMap.get(this);
        const staticListeners = [...listeners];
        const staticAnyListeners = [...anyListeners];
        triggeredEventsMap.set(eventName, { data: eventData });
        await resolvedPromise;
        return Promise.all([
            ...staticListeners.map(async (listener) => {
                if (listeners.has(listener)) {
                    return listener(eventData);
                }
            }),
            ...staticAnyListeners.map(async (listener) => {
                if (anyListeners.has(listener)) {
                    return listener(eventName, eventData);
                }
            }),
        ]);
    }
    emitSync(eventName, eventData) {
        assertEventName(eventName);
        const listeners = getListeners(this, eventName);
        const triggeredEventsMap = triggeredMap.get(this);
        const anyListeners = anyMap.get(this);
        const staticListeners = [...listeners];
        const staticAnyListeners = [...anyListeners];
        triggeredEventsMap.set(eventName, { data: eventData });
        return [
            ...staticListeners.map((listener) => {
                if (listeners.has(listener)) {
                    return listener(eventData);
                }
            }),
            ...staticAnyListeners.map((listener) => {
                if (anyListeners.has(listener)) {
                    return listener(eventName, eventData);
                }
            }),
        ];
    }
    async emitSerial(eventName, eventData) {
        assertEventName(eventName);
        const listeners = getListeners(this, eventName);
        const anyListeners = anyMap.get(this);
        const staticListeners = [...listeners];
        const staticAnyListeners = [...anyListeners];
        await resolvedPromise;
        /* eslint-disable no-await-in-loop */
        for (const listener of staticListeners) {
            if (listeners.has(listener)) {
                await listener(eventData);
            }
        }
        for (const listener of staticAnyListeners) {
            if (anyListeners.has(listener)) {
                await listener(eventName, eventData);
            }
        }
        /* eslint-enable no-await-in-loop */
    }
    onAny(listener) {
        assertListener(listener);
        anyMap.get(this).add(listener);
        return this.offAny.bind(this, listener);
    }
    offAny(listener) {
        assertListener(listener);
        anyMap.get(this).delete(listener);
    }
    clearListeners(eventName) {
        if (typeof eventName === 'string') {
            getListeners(this, eventName).clear();
        }
        else {
            anyMap.get(this).clear();
            for (const listeners of eventsMap.get(this).values()) {
                listeners.clear();
            }
        }
    }
    listenerCount(eventName) {
        if (typeof eventName === 'string') {
            return anyMap.get(this).size + getListeners(this, eventName).size;
        }
        if (typeof eventName !== 'undefined') {
            assertEventName(eventName);
        }
        let count = anyMap.get(this).size;
        for (const value of eventsMap.get(this).values()) {
            count += value.size;
        }
        return count;
    }
    bindMethods(target, methodNames) {
        if (typeof target !== 'object' || target === null) {
            throw new TypeError('`target` must be an object');
        }
        methodNames = defaultMethodNamesOrAssert(methodNames);
        for (const methodName of methodNames) {
            if (target[methodName] !== undefined) {
                throw new Error(`The property \`${methodName}\` already exists on \`target\``);
            }
            Object.defineProperty(target, methodName, {
                enumerable: false,
                value: this[methodName].bind(this),
            });
        }
    }
}
const allEmitteryMethods = Object.getOwnPropertyNames(Emittery.prototype).filter((v) => v !== 'constructor');
exports.default = Emittery;
