import * as PropertySymbol from '../PropertySymbol.js';
import Event from './Event.js';
import EventPhaseEnum from './EventPhaseEnum.js';
import WindowBrowserContext from '../window/WindowBrowserContext.js';
import BrowserErrorCaptureEnum from '../browser/enums/BrowserErrorCaptureEnum.js';
/**
 * Handles events.
 */
export default class EventTarget {
    [PropertySymbol.listeners] = {
        capturing: new Map(),
        bubbling: new Map()
    };
    [PropertySymbol.listenerOptions] = {
        capturing: new Map(),
        bubbling: new Map()
    };
    /**
     * Return a default description for the EventTarget class.
     */
    get [Symbol.toStringTag]() {
        return 'EventTarget';
    }
    /**
     * Adds an event listener.
     *
     * @param type Event type.
     * @param listener Listener.
     * @param options An object that specifies characteristics about the event listener.(currently only once)
     * @param options.once
     * @param options.signal An AbortSignal. The listener will be removed when the given AbortSignal object's abort() method is called.
     */
    addEventListener(type, listener, options) {
        options = typeof options === 'boolean' ? { capture: options } : options || {};
        const eventPhase = options.capture ? 'capturing' : 'bubbling';
        let listeners = this[PropertySymbol.listeners][eventPhase].get(type);
        let listenerOptions;
        if (listeners) {
            listenerOptions = this[PropertySymbol.listenerOptions][eventPhase].get(type);
        }
        else {
            listeners = [];
            listenerOptions = [];
            this[PropertySymbol.listeners][eventPhase].set(type, listeners);
            this[PropertySymbol.listenerOptions][eventPhase].set(type, listenerOptions);
        }
        if (listeners.includes(listener)) {
            return;
        }
        listeners.push(listener);
        listenerOptions.push(options);
        if (options.signal && !options.signal.aborted) {
            options.signal.addEventListener('abort', () => {
                this.removeEventListener(type, listener);
            });
        }
    }
    /**
     * Adds an event listener.
     *
     * @param type Event type.
     * @param listener Listener.
     */
    removeEventListener(type, listener) {
        const bubblingListeners = this[PropertySymbol.listeners].bubbling.get(type);
        if (bubblingListeners) {
            const index = bubblingListeners.indexOf(listener);
            if (index !== -1) {
                bubblingListeners.splice(index, 1);
                this[PropertySymbol.listenerOptions].bubbling.get(type).splice(index, 1);
                return;
            }
        }
        const capturingListeners = this[PropertySymbol.listeners].capturing.get(type);
        if (capturingListeners) {
            const index = capturingListeners.indexOf(listener);
            if (index !== -1) {
                capturingListeners.splice(index, 1);
                this[PropertySymbol.listenerOptions].capturing.get(type).splice(index, 1);
            }
        }
    }
    /**
     * Dispatches an event.
     *
     * @see https://www.w3.org/TR/DOM-Level-3-Events/#event-flow
     * @see https://www.quirksmode.org/js/events_order.html#link4
     * @param event Event.
     * @returns The return value is false if event is cancelable and at least one of the event handlers which handled this event called Event.preventDefault().
     */
    dispatchEvent(event) {
        if (!(event instanceof Event)) {
            throw new this[PropertySymbol.window].TypeError(`Failed to execute 'dispatchEvent' on 'EventTarget': parameter 1 is not of type 'Event'.`);
        }
        // The "load" event is a special case. It should not bubble up to the window from the document.
        if (!event[PropertySymbol.dispatching] &&
            (event[PropertySymbol.type] !== 'load' || !event[PropertySymbol.target])) {
            event[PropertySymbol.dispatching] = true;
            event[PropertySymbol.target] = this[PropertySymbol.proxy] || this;
            this.#goThroughDispatchEventPhases(event);
            event[PropertySymbol.dispatching] = false;
            return !(event[PropertySymbol.cancelable] && event[PropertySymbol.defaultPrevented]);
        }
        this.#callDispatchEventListeners(event);
        return !(event[PropertySymbol.cancelable] && event[PropertySymbol.defaultPrevented]);
    }
    /**
     * Adds an event listener.
     *
     * TODO:
     * Was used by with IE8- and Opera. React believed Happy DOM was a legacy browser and used them, but that is no longer the case, so we should remove this method after that this is verified.
     *
     * @deprecated
     * @param type Event type.
     * @param listener Listener.
     */
    attachEvent(type, listener) {
        this.addEventListener(type.replace('on', ''), listener);
    }
    /**
     * Removes an event listener.
     *
     * TODO:
     * Was used by IE8- and Opera. React believed Happy DOM was a legacy browser and used them, but that is no longer the case, so we should remove this method after that this is verified.
     *
     * @deprecated
     * @param type Event type.
     * @param listener Listener.
     */
    detachEvent(type, listener) {
        this.removeEventListener(type.replace('on', ''), listener);
    }
    /**
     * Destroys the node.
     */
    [PropertySymbol.destroy]() {
        this[PropertySymbol.listeners].capturing.clear();
        this[PropertySymbol.listeners].bubbling.clear();
        this[PropertySymbol.listenerOptions].capturing.clear();
        this[PropertySymbol.listenerOptions].bubbling.clear();
    }
    /**
     * Goes through dispatch event phases.
     *
     * @param event Event.
     */
    #goThroughDispatchEventPhases(event) {
        const composedPath = event.composedPath();
        // Capturing phase
        event[PropertySymbol.eventPhase] = EventPhaseEnum.capturing;
        for (let i = composedPath.length - 1; i >= 0; i--) {
            event[PropertySymbol.currentTarget] = composedPath[i];
            composedPath[i].dispatchEvent(event);
            if (event[PropertySymbol.propagationStopped] ||
                event[PropertySymbol.immediatePropagationStopped]) {
                event[PropertySymbol.eventPhase] = EventPhaseEnum.none;
                event[PropertySymbol.currentTarget] = null;
                return;
            }
        }
        // At target phase
        event[PropertySymbol.eventPhase] = EventPhaseEnum.atTarget;
        event[PropertySymbol.currentTarget] = this[PropertySymbol.proxy] || this;
        event[PropertySymbol.target].dispatchEvent(event);
        // Bubbling phase
        event[PropertySymbol.eventPhase] = EventPhaseEnum.bubbling;
        if (event[PropertySymbol.bubbles] &&
            !event[PropertySymbol.propagationStopped] &&
            !event[PropertySymbol.immediatePropagationStopped]) {
            for (let i = 1, max = composedPath.length; i < max; i++) {
                event[PropertySymbol.currentTarget] = composedPath[i];
                composedPath[i].dispatchEvent(event);
                if (event[PropertySymbol.propagationStopped] ||
                    event[PropertySymbol.immediatePropagationStopped]) {
                    event[PropertySymbol.eventPhase] = EventPhaseEnum.none;
                    event[PropertySymbol.currentTarget] = null;
                    return;
                }
            }
        }
        // None phase (done)
        event[PropertySymbol.eventPhase] = EventPhaseEnum.none;
        event[PropertySymbol.currentTarget] = null;
    }
    /**
     * Handles dispatch event listeners.
     *
     * @param event Event.
     */
    #callDispatchEventListeners(event) {
        const window = this[PropertySymbol.window];
        const browserSettings = window ? new WindowBrowserContext(window).getSettings() : null;
        const eventPhase = event.eventPhase === EventPhaseEnum.capturing ? 'capturing' : 'bubbling';
        // We need to clone the arrays because the listeners may remove themselves while we are iterating.
        const listeners = this[PropertySymbol.listeners][eventPhase].get(event.type)?.slice();
        if (listeners && listeners.length) {
            const listenerOptions = this[PropertySymbol.listenerOptions][eventPhase]
                .get(event.type)
                .slice();
            for (let i = 0, max = listeners.length; i < max; i++) {
                const listener = listeners[i];
                const options = listenerOptions[i];
                if (options?.passive) {
                    event[PropertySymbol.isInPassiveEventListener] = true;
                }
                // We can end up in a never ending loop if the listener for the error event on Window also throws an error.
                if (window &&
                    (this !== window || event.type !== 'error') &&
                    !browserSettings?.disableErrorCapturing &&
                    browserSettings?.errorCapture === BrowserErrorCaptureEnum.tryAndCatch) {
                    if (listener.handleEvent) {
                        let result;
                        try {
                            result = listener.handleEvent.call(listener, event);
                        }
                        catch (error) {
                            window[PropertySymbol.dispatchError](error);
                        }
                        if (result instanceof Promise) {
                            result.catch((error) => window[PropertySymbol.dispatchError](error));
                        }
                    }
                    else {
                        let result;
                        try {
                            result = listener.call(this, event);
                        }
                        catch (error) {
                            window[PropertySymbol.dispatchError](error);
                        }
                        if (result instanceof Promise) {
                            result.catch((error) => window[PropertySymbol.dispatchError](error));
                        }
                    }
                }
                else {
                    if (listener.handleEvent) {
                        listener.handleEvent.call(listener, event);
                    }
                    else {
                        listener.call(this, event);
                    }
                }
                event[PropertySymbol.isInPassiveEventListener] = false;
                if (options?.once) {
                    // At this time, listeners and listenersOptions are cloned arrays. When the original value is deleted,
                    // The value corresponding to the cloned array is not deleted. So we need to delete the value in the cloned array.
                    listeners.splice(i, 1);
                    listenerOptions.splice(i, 1);
                    this.removeEventListener(event.type, listener);
                    i--;
                    max--;
                }
                if (event[PropertySymbol.immediatePropagationStopped]) {
                    return;
                }
            }
        }
        if (event.eventPhase !== EventPhaseEnum.capturing) {
            // Only call on* handlers if they are defined as part of the class interface (via
            // propertyEventListeners or as an own property on a non-EventTarget class), not arbitrary
            // properties assigned at runtime on plain EventTarget instances. (issue #1895)
            const onEventName = 'on' + event.type.toLowerCase();
            const propertyEventListeners = this[PropertySymbol.propertyEventListeners];
            // Check if this on* handler is defined as part of the class interface:
            // 1. Via propertyEventListeners Map (used by Element, Document, etc.)
            //    For these classes, we always try to get the listener since the getter handles
            //    lazy evaluation of attribute-based event listeners.
            // 2. As an own property on a subclass of EventTarget that defines on* handlers
            //    (used by MediaQueryList, AbortSignal, etc.)
            //    Plain EventTarget instances should not have arbitrary on* properties called.
            const hasClassDefinedHandler = !propertyEventListeners &&
                Object.hasOwn(this, onEventName) &&
                this.constructor.name !== 'EventTarget';
            if (propertyEventListeners || hasClassDefinedHandler) {
                const eventListener = propertyEventListeners?.get(onEventName) ?? this[onEventName];
                if (typeof eventListener === 'function') {
                    // We can end up in a never ending loop if the listener for the error event on Window also throws an error.
                    if (window &&
                        (this !== window || event.type !== 'error') &&
                        !browserSettings?.disableErrorCapturing &&
                        browserSettings?.errorCapture === BrowserErrorCaptureEnum.tryAndCatch) {
                        let result;
                        try {
                            result = eventListener(event);
                        }
                        catch (error) {
                            window[PropertySymbol.dispatchError](error);
                        }
                        if (result instanceof Promise) {
                            result.catch((error) => window[PropertySymbol.dispatchError](error));
                        }
                    }
                    else {
                        eventListener(event);
                    }
                }
            }
        }
    }
}
//# sourceMappingURL=EventTarget.js.map