"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InvalidArgumentError = exports.inspect = exports.removeListener = void 0;
const is_js_1 = require("./is.js");
function callListener(eventThis, listener, args) {
    try {
        listener.apply(eventThis, args);
    }
    catch (e) {
        console.error('EventEmitter.emit()', 'Unexpected listener exception: ' + e + '; stack = ' + (e && e.stack));
    }
}
/**
 * Remove listeners that match listener
 * @param targetListeners is an array of listener arrays or event objects with arrays of listeners
 * @param listener the listener callback to remove
 * @param eventFilter (optional) event name instructing the function to only remove listeners for the specified event
 */
function removeListener(targetListeners, listener, eventFilter) {
    let listeners;
    let index;
    let eventName;
    for (let targetListenersIndex = 0; targetListenersIndex < targetListeners.length; targetListenersIndex++) {
        listeners = targetListeners[targetListenersIndex];
        if ((0, is_js_1.isString)(eventFilter) && (0, is_js_1.isObject)(listeners)) {
            listeners = listeners[eventFilter];
        }
        if ((0, is_js_1.isArray)(listeners)) {
            while ((index = listeners.indexOf(listener)) !== -1) {
                listeners.splice(index, 1);
            }
            /* If events object has an event name key with no listeners then
                       remove the key to stop the list growing indefinitely */
            const parentCollection = targetListeners[targetListenersIndex];
            if (eventFilter && listeners.length === 0 && (0, is_js_1.isObject)(parentCollection)) {
                delete parentCollection[eventFilter];
            }
        }
        else if ((0, is_js_1.isObject)(listeners)) {
            for (eventName in listeners) {
                if (Object.prototype.hasOwnProperty.call(listeners, eventName) && (0, is_js_1.isArray)(listeners[eventName])) {
                    removeListener([listeners], listener, eventName);
                }
            }
        }
    }
}
exports.removeListener = removeListener;
// Equivalent of Platform.config.inspect from ably-js for browser/RN
function inspect(args) {
    return JSON.stringify(args);
}
exports.inspect = inspect;
class InvalidArgumentError extends Error {
    constructor(...args) {
        super(...args);
    }
}
exports.InvalidArgumentError = InvalidArgumentError;
class EventEmitter {
    constructor() {
        this.any = [];
        this.events = Object.create(null);
        this.anyOnce = [];
        this.eventsOnce = Object.create(null);
    }
    /**
     * Add an event listener
     * @param listenerOrEvents (optional) the name of the event to listen to or the listener to be called.
     * @param listener (optional) the listener to be called.
     */
    on(listenerOrEvents, listener) {
        // .on(() => {})
        if ((0, is_js_1.isFunction)(listenerOrEvents)) {
            this.any.push(listenerOrEvents);
            return;
        }
        // .on("eventName", () => {})
        if ((0, is_js_1.isString)(listenerOrEvents) && (0, is_js_1.isFunction)(listener)) {
            const listeners = this.events[listenerOrEvents] || (this.events[listenerOrEvents] = []);
            listeners.push(listener);
            return;
        }
        // .on(["eventName"], () => {})
        if ((0, is_js_1.isArray)(listenerOrEvents) && (0, is_js_1.isFunction)(listener)) {
            listenerOrEvents.forEach((eventName) => {
                this.on(eventName, listener);
            });
            return;
        }
        throw new InvalidArgumentError('EventEmitter.on(): Invalid arguments: ' + inspect([listenerOrEvents, listener]));
    }
    /**
     * Remove one or more event listeners
     * @param listenerOrEvents (optional) the name of the event whose listener is to be removed. If not supplied,
     * the listener is treated as an 'any' listener.
     * @param listener (optional) the listener to remove. If not supplied, all listeners are removed.
     */
    off(listenerOrEvents, listener) {
        // .off()
        // don't use arguments.length === 0 here as don't won't handle
        // cases like .off(undefined) which is a valid call
        if (!listenerOrEvents && !listener) {
            this.any = [];
            this.events = Object.create(null);
            this.anyOnce = [];
            this.eventsOnce = Object.create(null);
            return;
        }
        // .off(() => {})
        if ((0, is_js_1.isFunction)(listenerOrEvents)) {
            removeListener([this.any, this.events, this.anyOnce, this.eventsOnce], listenerOrEvents);
            return;
        }
        // .off("eventName", () => {})
        if ((0, is_js_1.isString)(listenerOrEvents) && (0, is_js_1.isFunction)(listener)) {
            removeListener([this.events, this.eventsOnce], listener, listenerOrEvents);
            return;
        }
        // .off("eventName")
        if ((0, is_js_1.isString)(listenerOrEvents)) {
            delete this.events[listenerOrEvents];
            delete this.eventsOnce[listenerOrEvents];
            return;
        }
        // .off(["eventName"], () => {})
        if ((0, is_js_1.isArray)(listenerOrEvents) && (0, is_js_1.isFunction)(listener)) {
            listenerOrEvents.forEach((eventName) => {
                this.off(eventName, listener);
            });
            return;
        }
        // .off(["eventName"])
        if ((0, is_js_1.isArray)(listenerOrEvents)) {
            listenerOrEvents.forEach((eventName) => {
                this.off(eventName);
            });
            return;
        }
        throw new InvalidArgumentError('EventEmitter.off(): invalid arguments:' + inspect([listenerOrEvents, listener]));
    }
    /**
     * Get the array of listeners for a given event; excludes once events
     * @param event (optional) the name of the event, or none for 'any'
     * @return array of events, or null if none
     */
    listeners(event) {
        var _a;
        if (event) {
            const listeners = [...((_a = this.events[event]) !== null && _a !== void 0 ? _a : [])];
            if ((0, is_js_1.isArray)(this.eventsOnce[event])) {
                Array.prototype.push.apply(listeners, this.eventsOnce[event]);
            }
            return listeners.length ? listeners : null;
        }
        return this.any.length ? this.any : null;
    }
    /**
     * Emit an event
     * @param event the event name
     * @param arg the arguments to pass to the listener
     */
    emit(event, arg) {
        const eventThis = { event };
        const listeners = [];
        if (this.anyOnce.length > 0) {
            Array.prototype.push.apply(listeners, this.anyOnce);
            this.anyOnce = [];
        }
        if (this.any.length > 0) {
            Array.prototype.push.apply(listeners, this.any);
        }
        const eventsOnceListeners = this.eventsOnce[event];
        if (eventsOnceListeners) {
            Array.prototype.push.apply(listeners, eventsOnceListeners);
            delete this.eventsOnce[event];
        }
        const eventsListeners = this.events[event];
        if (eventsListeners) {
            Array.prototype.push.apply(listeners, eventsListeners);
        }
        listeners.forEach(function (listener) {
            callListener(eventThis, listener, [arg]);
        });
    }
    /**
     * Listen for a single occurrence of an event
     * @param listenerOrEvents (optional) the name of the event to listen to
     * @param listener (optional) the listener to be called
     */
    once(listenerOrEvents, listener) {
        // .once("eventName", () => {})
        if ((0, is_js_1.isString)(listenerOrEvents) && (0, is_js_1.isFunction)(listener)) {
            const listeners = this.eventsOnce[listenerOrEvents] || (this.eventsOnce[listenerOrEvents] = []);
            listeners.push(listener);
            return;
        }
        // .once(["eventName"], () => {})
        if ((0, is_js_1.isArray)(listenerOrEvents) && (0, is_js_1.isFunction)(listener)) {
            const self = this;
            listenerOrEvents.forEach(function (eventName) {
                const listenerWrapper = function (listenerThis) {
                    const innerArgs = Array.prototype.slice.call(arguments);
                    listenerOrEvents.forEach((eventName) => {
                        self.off(eventName, this);
                    });
                    listener.apply(listenerThis, innerArgs);
                };
                self.once(eventName, listenerWrapper);
            });
            return;
        }
        // .once(() => {})
        if ((0, is_js_1.isFunction)(listenerOrEvents)) {
            this.anyOnce.push(listenerOrEvents);
            return;
        }
        throw new InvalidArgumentError('EventEmitter.once(): invalid arguments:' + inspect([listenerOrEvents, listener]));
    }
    /**
     * Private API
     *
     * Listen for a single occurrence of a state event and fire immediately if currentState matches targetState
     * @param targetState the name of the state event to listen to
     * @param currentState the name of the current state of this object
     * @param listener the listener to be called
     * @param listenerArgs
     */
    whenState(targetState, currentState, listener, ...listenerArgs) {
        const eventThis = { event: targetState };
        if (typeof targetState !== 'string' || typeof currentState !== 'string') {
            throw new InvalidArgumentError('whenState requires a valid event String argument');
        }
        if (typeof listener !== 'function' && Promise) {
            return new Promise((resolve) => {
                EventEmitter.prototype.whenState.apply(this, [targetState, currentState, resolve].concat(listenerArgs));
            });
        }
        if (targetState === currentState) {
            callListener(eventThis, listener, listenerArgs);
        }
        else {
            this.once(targetState, listener);
        }
    }
}
exports.default = EventEmitter;
//# sourceMappingURL=EventEmitter.js.map