/**
 * EventHandler callback
 * @typedef {(event: {[key: string]: any}): void} EventHandlerCallbackType
 */

const sleep = (t) => new Promise((r) => setTimeout(() => r(), t));

/**
 * Inherited class for RealtimeAPI and RealtimeClient
 * Adds basic event handling
 * @class
 */
export class RealtimeEventHandler {
    /**
     * Create a new RealtimeEventHandler instance
     * @returns {RealtimeEventHandler}
     */
    constructor() {
        this.eventHandlers = {};
        this.nextEventHandlers = {};
    }

    /**
     * Clears all event handlers
     * @returns {true}
     */
    clearEventHandlers() {
        this.eventHandlers = {};
        this.nextEventHandlers = {};
        return true;
    }

    /**
     * Listen to specific events
     * @param {string} eventName The name of the event to listen to
     * @param {EventHandlerCallbackType} callback Code to execute on event
     * @returns {EventHandlerCallbackType}
     */
    on(eventName, callback) {
        this.eventHandlers[eventName] = this.eventHandlers[eventName] || [];
        this.eventHandlers[eventName].push(callback);
        return callback;
    }

    /**
     * Listen for the next event of a specified type
     * @param {string} eventName The name of the event to listen to
     * @param {EventHandlerCallbackType} callback Code to execute on event
     * @returns {EventHandlerCallbackType}
     */
    onNext(eventName, callback) {
        this.nextEventHandlers[eventName] = this.nextEventHandlers[eventName] || [];
        this.nextEventHandlers[eventName].push(callback);
        return callback;
    }

    /**
     * Turns off event listening for specific events
     * Calling without a callback will remove all listeners for the event
     * @param {string} eventName
     * @param {EventHandlerCallbackType} [callback]
     * @returns {true}
     */
    off(eventName, callback) {
        const handlers = this.eventHandlers[eventName] || [];
        if (callback) {
            const index = handlers.indexOf(callback);
            if (index === -1) {
                throw new Error(
                    `Could not turn off specified event listener for "${eventName}": not found as a listener`,
                );
            }
            handlers.splice(index, 1);
        } else {
            delete this.eventHandlers[eventName];
        }
        return true;
    }

    /**
     * Turns off event listening for the next event of a specific type
     * Calling without a callback will remove all listeners for the next event
     * @param {string} eventName
     * @param {EventHandlerCallbackType} [callback]
     * @returns {true}
     */
    offNext(eventName, callback) {
        const nextHandlers = this.nextEventHandlers[eventName] || [];
        if (callback) {
            const index = nextHandlers.indexOf(callback);
            if (index === -1) {
                throw new Error(
                    `Could not turn off specified next event listener for "${eventName}": not found as a listener`,
                );
            }
            nextHandlers.splice(index, 1);
        } else {
            delete this.nextEventHandlers[eventName];
        }
        return true;
    }

    /**
     * Waits for next event of a specific type and returns the payload
     * @param {string} eventName
     * @param {number|null} [timeout]
     * @returns {Promise<{[key: string]: any}|null>}
     */
    async waitForNext(eventName, timeout = null) {
        const t0 = Date.now();
        let nextEvent;
        this.onNext(eventName, (event) => (nextEvent = event));
        while (!nextEvent) {
            if (timeout) {
                const t1 = Date.now();
                if (t1 - t0 > timeout) {
                    return null;
                }
            }
            await sleep(1);
        }
        return nextEvent;
    }

    /**
     * Executes all events in the order they were added, with .on() event handlers executing before .onNext() handlers
     * @param {string} eventName
     * @param {any} event
     * @returns {true}
     */
    dispatch(eventName, event) {
        const handlers = [].concat(this.eventHandlers[eventName] || []);
        for (const handler of handlers) {
            handler(event);
        }
        const nextHandlers = [].concat(this.nextEventHandlers[eventName] || []);
        for (const nextHandler of nextHandlers) {
            nextHandler(event);
        }
        delete this.nextEventHandlers[eventName];
        return true;
    }
}
