"use strict";
/**
 * Copyright 2022 Google LLC.
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.SubscriptionManager = void 0;
exports.cartesianProduct = cartesianProduct;
exports.unrollEvents = unrollEvents;
exports.difference = difference;
const protocol_js_1 = require("../../../protocol/protocol.js");
const uuid_js_1 = require("../../../utils/uuid.js");
/**
 * Returns the cartesian product of the given arrays.
 *
 * Example:
 *   cartesian([1, 2], ['a', 'b']); => [[1, 'a'], [1, 'b'], [2, 'a'], [2, 'b']]
 */
function cartesianProduct(...a) {
    return a.reduce((a, b) => a.flatMap((d) => b.map((e) => [d, e].flat())));
}
/** Expands "AllEvents" events into atomic events. */
function unrollEvents(events) {
    const allEvents = new Set();
    function addEvents(events) {
        for (const event of events) {
            allEvents.add(event);
        }
    }
    for (const event of events) {
        switch (event) {
            case protocol_js_1.ChromiumBidi.BiDiModule.Bluetooth:
                addEvents(Object.values(protocol_js_1.ChromiumBidi.Bluetooth.EventNames));
                break;
            case protocol_js_1.ChromiumBidi.BiDiModule.BrowsingContext:
                addEvents(Object.values(protocol_js_1.ChromiumBidi.BrowsingContext.EventNames));
                break;
            case protocol_js_1.ChromiumBidi.BiDiModule.Input:
                addEvents(Object.values(protocol_js_1.ChromiumBidi.Input.EventNames));
                break;
            case protocol_js_1.ChromiumBidi.BiDiModule.Log:
                addEvents(Object.values(protocol_js_1.ChromiumBidi.Log.EventNames));
                break;
            case protocol_js_1.ChromiumBidi.BiDiModule.Network:
                addEvents(Object.values(protocol_js_1.ChromiumBidi.Network.EventNames));
                break;
            case protocol_js_1.ChromiumBidi.BiDiModule.Script:
                addEvents(Object.values(protocol_js_1.ChromiumBidi.Script.EventNames));
                break;
            case protocol_js_1.ChromiumBidi.BiDiModule.Speculation:
                addEvents(Object.values(protocol_js_1.ChromiumBidi.Speculation.EventNames));
                break;
            default:
                allEvents.add(event);
        }
    }
    return allEvents.values();
}
class SubscriptionManager {
    #subscriptions = [];
    #knownSubscriptionIds = new Set();
    #browsingContextStorage;
    constructor(browsingContextStorage) {
        this.#browsingContextStorage = browsingContextStorage;
    }
    getGoogChannelsSubscribedToEvent(eventName, contextId) {
        const googChannels = new Set();
        for (const subscription of this.#subscriptions) {
            if (this.#isSubscribedTo(subscription, eventName, contextId)) {
                googChannels.add(subscription.googChannel);
            }
        }
        return Array.from(googChannels);
    }
    getGoogChannelsSubscribedToEventGlobally(eventName) {
        const googChannels = new Set();
        for (const subscription of this.#subscriptions) {
            if (this.#isSubscribedTo(subscription, eventName)) {
                googChannels.add(subscription.googChannel);
            }
        }
        return Array.from(googChannels);
    }
    #isSubscribedTo(subscription, moduleOrEvent, browsingContextId) {
        let includesEvent = false;
        for (const eventName of subscription.eventNames) {
            // This also covers the `goog:cdp` case where
            // we don't unroll the event names
            if (
            // Event explicitly subscribed
            eventName === moduleOrEvent ||
                // Event subscribed via module
                eventName === moduleOrEvent.split('.').at(0) ||
                // Event explicitly subscribed compared to module
                eventName.split('.').at(0) === moduleOrEvent) {
                includesEvent = true;
                break;
            }
        }
        if (!includesEvent) {
            return false;
        }
        // user context subscription.
        if (subscription.userContextIds.size !== 0) {
            if (!browsingContextId) {
                return false;
            }
            const context = this.#browsingContextStorage.findContext(browsingContextId);
            if (!context) {
                return false;
            }
            return subscription.userContextIds.has(context.userContext);
        }
        // context subscription.
        if (subscription.topLevelTraversableIds.size !== 0) {
            if (!browsingContextId) {
                return false;
            }
            const topLevelContext = this.#browsingContextStorage.findTopLevelContextId(browsingContextId);
            return (topLevelContext !== null &&
                subscription.topLevelTraversableIds.has(topLevelContext));
        }
        // global subscription.
        return true;
    }
    isSubscribedTo(moduleOrEvent, contextId) {
        for (const subscription of this.#subscriptions) {
            if (this.#isSubscribedTo(subscription, moduleOrEvent, contextId)) {
                return true;
            }
        }
        return false;
    }
    /**
     * Subscribes to event in the given context and goog:channel.
     * @return {SubscriptionItem[]} List of
     * subscriptions. If the event is a whole module, it will return all the specific
     * events. If the contextId is null, it will return all the top-level contexts which were
     * not subscribed before the command.
     */
    subscribe(eventNames, contextIds, userContextIds, googChannel) {
        // All the subscriptions are handled on the top-level contexts.
        const subscription = {
            id: (0, uuid_js_1.uuidv4)(),
            eventNames: new Set(unrollEvents(eventNames)),
            topLevelTraversableIds: new Set(contextIds.map((contextId) => {
                const topLevelContext = this.#browsingContextStorage.findTopLevelContextId(contextId);
                if (!topLevelContext) {
                    throw new protocol_js_1.NoSuchFrameException(`Top-level navigable not found for context id ${contextId}`);
                }
                return topLevelContext;
            })),
            userContextIds: new Set(userContextIds),
            googChannel,
        };
        this.#subscriptions.push(subscription);
        this.#knownSubscriptionIds.add(subscription.id);
        return subscription;
    }
    /**
     * Unsubscribes atomically from all events in the given contexts and channel.
     *
     * This is a legacy spec branch to unsubscribe by attributes.
     */
    unsubscribe(inputEventNames, googChannel) {
        const eventNames = new Set(unrollEvents(inputEventNames));
        const newSubscriptions = [];
        const eventsMatched = new Set();
        for (const subscription of this.#subscriptions) {
            if (subscription.googChannel !== googChannel) {
                newSubscriptions.push(subscription);
                continue;
            }
            // Skip user context subscriptions.
            if (subscription.userContextIds.size !== 0) {
                newSubscriptions.push(subscription);
                continue;
            }
            // Skip subscriptions when none of the event names match.
            if (intersection(subscription.eventNames, eventNames).size === 0) {
                newSubscriptions.push(subscription);
                continue;
            }
            // Skip non-global subscriptions.
            if (subscription.topLevelTraversableIds.size !== 0) {
                newSubscriptions.push(subscription);
                continue;
            }
            const subscriptionEventNames = new Set(subscription.eventNames);
            for (const eventName of eventNames) {
                if (subscriptionEventNames.has(eventName)) {
                    eventsMatched.add(eventName);
                    subscriptionEventNames.delete(eventName);
                }
            }
            if (subscriptionEventNames.size !== 0) {
                newSubscriptions.push({
                    ...subscription,
                    eventNames: subscriptionEventNames,
                });
            }
        }
        // If some events did not match, it is an invalid request.
        if (!equal(eventsMatched, eventNames)) {
            throw new protocol_js_1.InvalidArgumentException('No subscription found');
        }
        // Committing the new subscriptions.
        this.#subscriptions = newSubscriptions;
    }
    /**
     * Unsubscribes by subscriptionId.
     */
    unsubscribeById(subscriptionIds) {
        const subscriptionIdsSet = new Set(subscriptionIds);
        const unknownIds = difference(subscriptionIdsSet, this.#knownSubscriptionIds);
        if (unknownIds.size !== 0) {
            throw new protocol_js_1.InvalidArgumentException('No subscription found');
        }
        this.#subscriptions = this.#subscriptions.filter((subscription) => {
            return !subscriptionIdsSet.has(subscription.id);
        });
        this.#knownSubscriptionIds = difference(this.#knownSubscriptionIds, subscriptionIdsSet);
    }
}
exports.SubscriptionManager = SubscriptionManager;
/**
 * Replace with Set.prototype.intersection once Node 20 is dropped.
 */
function intersection(setA, setB) {
    const result = new Set();
    for (const a of setA) {
        if (setB.has(a)) {
            result.add(a);
        }
    }
    return result;
}
/**
 * Replace with Set.prototype.difference once Node 20 is dropped.
 */
function difference(setA, setB) {
    const result = new Set();
    for (const a of setA) {
        if (!setB.has(a)) {
            result.add(a);
        }
    }
    return result;
}
function equal(setA, setB) {
    if (setA.size !== setB.size) {
        return false;
    }
    for (const a of setA) {
        if (!setB.has(a)) {
            return false;
        }
    }
    return true;
}
//# sourceMappingURL=SubscriptionManager.js.map