/*
 * Copyright (C) 2008, 2013 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

WI.Object = class WebInspectorObject
{
    constructor()
    {
        this._listeners = null;
    }

    // Static

    static addEventListener(eventType, listener, thisObject)
    {
        console.assert(typeof eventType === "string", this, eventType, listener, thisObject);
        console.assert(typeof listener === "function", this, eventType, listener, thisObject);
        console.assert(typeof thisObject === "object" || window.InspectorTest || window.ProtocolTest, this, eventType, listener, thisObject);

        thisObject ??= this;

        let data = {
            listener,
            thisObjectWeakRef: new WeakRef(thisObject),
        };

        WI.Object._listenerThisObjectFinalizationRegistry.register(thisObject, {eventTargetWeakRef: new WeakRef(this), eventType, data}, data);

        this._listeners ??= new Multimap;
        this._listeners.add(eventType, data);

        console.assert(Array.from(this._listeners.get(eventType)).filter((item) => item.listener === listener && item.thisObjectWeakRef.deref() === thisObject).length === 1, this, eventType, listener, thisObject);

        return listener;
    }

    static singleFireEventListener(eventType, listener, thisObject)
    {
        let eventTargetWeakRef = new WeakRef(this);
        return this.addEventListener(eventType, function wrappedCallback() {
            eventTargetWeakRef.deref()?.removeEventListener(eventType, wrappedCallback, this);
            listener.apply(this, arguments);
        }, thisObject);
    }

    static awaitEvent(eventType, thisObject)
    {
        return new Promise((resolve, reject) => {
            this.singleFireEventListener(eventType, resolve, thisObject);
        });
    }

    static removeEventListener(eventType, listener, thisObject)
    {
        console.assert(this._listeners, this, eventType, listener, thisObject);
        console.assert(typeof eventType === "string", this, eventType, listener, thisObject);
        console.assert(typeof listener === "function", this, eventType, listener, thisObject);
        console.assert(typeof thisObject === "object" || window.InspectorTest || window.ProtocolTest, this, eventType, listener, thisObject);

        if (!this._listeners)
            return;

        thisObject ??= this;

        let listenersForEventType = this._listeners.get(eventType);
        console.assert(listenersForEventType, this, eventType, listener, thisObject);
        if (!listenersForEventType)
            return;

        let didDelete = false;
        for (let data of listenersForEventType) {
            let unwrapped = data.thisObjectWeakRef.deref();
            if (!unwrapped || unwrapped !== thisObject || data.listener !== listener)
                continue;

            if (this._listeners.delete(eventType, data))
                didDelete = true;
            WI.Object._listenerThisObjectFinalizationRegistry.unregister(data);
        }
        console.assert(didDelete, this, eventType, listener, thisObject);
    }

    // Public

    addEventListener() { return WI.Object.addEventListener.apply(this, arguments); }
    singleFireEventListener() { return WI.Object.singleFireEventListener.apply(this, arguments); }
    awaitEvent() { return WI.Object.awaitEvent.apply(this, arguments); }
    removeEventListener() { return WI.Object.removeEventListener.apply(this, arguments); }

    dispatchEventToListeners(eventType, eventData)
    {
        let event = new WI.Event(this, eventType, eventData);

        function dispatch(object)
        {
            if (!object || event._stoppedPropagation)
                return;

            let listeners = object._listeners;
            if (!listeners || !object.hasOwnProperty("_listeners") || !listeners.size)
                return;

            let listenersForEventType = listeners.get(eventType);
            if (!listenersForEventType)
                return;

            // Copy the set of listeners so we don't have to worry about mutating while iterating.
            for (let data of Array.from(listenersForEventType)) {
                let unwrapped = data.thisObjectWeakRef.deref();
                if (!unwrapped)
                    continue;

                data.listener.call(unwrapped, event);

                if (event._stoppedPropagation)
                    break;
            }
        }

        // Dispatch to listeners of this specific object.
        dispatch(this);

        // Allow propagation again so listeners on the constructor always have a crack at the event.
        event._stoppedPropagation = false;

        // Dispatch to listeners on all constructors up the prototype chain, including the immediate constructor.
        let constructor = this.constructor;
        while (constructor) {
            dispatch(constructor);

            if (!constructor.prototype.__proto__)
                break;

            constructor = constructor.prototype.__proto__.constructor;
        }

        return event.defaultPrevented;
    }

    // Test

    static hasEventListeners(eventType)
    {
        console.assert(window.InspectorTest || window.ProtocolTest);
        return this._listeners?.has(eventType);
    }

    static activelyListeningObjectsWithPrototype(proto)
    {
        console.assert(window.InspectorTest || window.ProtocolTest);
        let results = new Set;
        if (this._listeners) {
            for (let data of this._listeners.values()) {
                let unwrapped = data.thisObjectWeakRef.deref();
                if (unwrapped instanceof proto)
                    results.add(unwrapped);
            }
        }
        return results;
    }

    hasEventListeners() { return WI.Object.hasEventListeners.apply(this, arguments); }
    activelyListeningObjectsWithPrototype() { return WI.Object.activelyListeningObjectsWithPrototype.apply(this, arguments); }
};

WI.Object._listenerThisObjectFinalizationRegistry = new FinalizationRegistry((heldValue) => {
    heldValue.eventTargetWeakRef.deref()?._listeners.delete(heldValue.eventType, heldValue.data);
});

WI.Event = class Event
{
    constructor(target, type, data)
    {
        this.target = target;
        this.type = type;
        this.data = data;
        this.defaultPrevented = false;
        this._stoppedPropagation = false;
    }

    stopPropagation()
    {
        this._stoppedPropagation = true;
    }

    preventDefault()
    {
        this.defaultPrevented = true;
    }
};

WI.notifications = new WI.Object;

WI.Notification = {
    GlobalModifierKeysDidChange: "global-modifiers-did-change",
    PageArchiveStarted: "page-archive-started",
    PageArchiveEnded: "page-archive-ended",
    ExtraDomainsActivated: "extra-domains-activated", // COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type
    VisibilityStateDidChange: "visibility-state-did-change",
    TransitionPageTarget: "transition-page-target",
};
