/**
 * 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.
 */
import { ChromiumBidi, InvalidArgumentException, } from '../../../protocol/protocol.js';
import { isCdpEvent, isDeprecatedCdpEvent } from './events.js';
/**
 * Returns the cartesian product of the given arrays.
 *
 * Example:
 *   cartesian([1, 2], ['a', 'b']); => [[1, 'a'], [1, 'b'], [2, 'a'], [2, 'b']]
 */
export function cartesianProduct(...a) {
    return a.reduce((a, b) => a.flatMap((d) => b.map((e) => [d, e].flat())));
}
/** Expands "AllEvents" events into atomic events. */
export 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 ChromiumBidi.BiDiModule.Bluetooth:
                addEvents(Object.values(ChromiumBidi.Bluetooth.EventNames));
                break;
            case ChromiumBidi.BiDiModule.BrowsingContext:
                addEvents(Object.values(ChromiumBidi.BrowsingContext.EventNames));
                break;
            case ChromiumBidi.BiDiModule.Log:
                addEvents(Object.values(ChromiumBidi.Log.EventNames));
                break;
            case ChromiumBidi.BiDiModule.Network:
                addEvents(Object.values(ChromiumBidi.Network.EventNames));
                break;
            case ChromiumBidi.BiDiModule.Script:
                addEvents(Object.values(ChromiumBidi.Script.EventNames));
                break;
            default:
                allEvents.add(event);
        }
    }
    return [...allEvents.values()];
}
export class SubscriptionManager {
    #subscriptionPriority = 0;
    // BrowsingContext `null` means the event has subscription across all the
    // browsing contexts.
    // Channel `null` means no `channel` should be added.
    #channelToContextToEventMap = new Map();
    #browsingContextStorage;
    constructor(browsingContextStorage) {
        this.#browsingContextStorage = browsingContextStorage;
    }
    getChannelsSubscribedToEvent(eventMethod, contextId) {
        const prioritiesAndChannels = Array.from(this.#channelToContextToEventMap.keys())
            .map((channel) => ({
            priority: this.#getEventSubscriptionPriorityForChannel(eventMethod, contextId, channel),
            channel,
        }))
            .filter(({ priority }) => priority !== null);
        // Sort channels by priority.
        return prioritiesAndChannels
            .sort((a, b) => a.priority - b.priority)
            .map(({ channel }) => channel);
    }
    #getEventSubscriptionPriorityForChannel(eventMethod, contextId, channel) {
        const contextToEventMap = this.#channelToContextToEventMap.get(channel);
        if (contextToEventMap === undefined) {
            return null;
        }
        const maybeTopLevelContextId = this.#browsingContextStorage.findTopLevelContextId(contextId);
        // `null` covers global subscription.
        const relevantContexts = [...new Set([null, maybeTopLevelContextId])];
        // Get all the subscription priorities.
        const priorities = relevantContexts
            .map((context) => {
            // Get the priority for exact event name
            const priority = contextToEventMap.get(context)?.get(eventMethod);
            // For CDP we can't provide specific event name when subscribing
            // to the module directly.
            // Because of that we need to see event `cdp` exists in the map.
            if (isCdpEvent(eventMethod)) {
                const cdpPriority = contextToEventMap
                    .get(context)
                    ?.get(ChromiumBidi.BiDiModule.Cdp);
                // If we subscribe to the event directly and `cdp` module as well
                // priority will be different we take minimal priority
                return priority && cdpPriority
                    ? Math.min(priority, cdpPriority)
                    : // At this point we know that we have subscribed
                        // to only one of the two
                        (priority ?? cdpPriority);
            }
            // https://github.com/GoogleChromeLabs/chromium-bidi/issues/2844.
            if (isDeprecatedCdpEvent(eventMethod)) {
                const cdpPriority = contextToEventMap
                    .get(context)
                    ?.get(ChromiumBidi.BiDiModule.DeprecatedCdp);
                // If we subscribe to the event directly and `cdp` module as well
                // priority will be different we take minimal priority
                return priority && cdpPriority
                    ? Math.min(priority, cdpPriority)
                    : // At this point we know that we have subscribed
                        // to only one of the two
                        (priority ?? cdpPriority);
            }
            return priority;
        })
            .filter((p) => p !== undefined);
        if (priorities.length === 0) {
            // Not subscribed, return null.
            return null;
        }
        // Return minimal priority.
        return Math.min(...priorities);
    }
    /**
     * @param module BiDi+ module
     * @param contextId `null` == globally subscribed
     *
     * @returns
     */
    isSubscribedTo(moduleOrEvent, contextId = null) {
        const topLevelContext = this.#browsingContextStorage.findTopLevelContextId(contextId);
        for (const browserContextToEventMap of this.#channelToContextToEventMap.values()) {
            for (const [id, eventMap] of browserContextToEventMap.entries()) {
                // Not subscribed to this context or globally
                if (topLevelContext !== id && id !== null) {
                    continue;
                }
                for (const event of eventMap.keys()) {
                    // This also covers the `cdp` case where
                    // we don't unroll the event names
                    if (
                    // Event explicitly subscribed
                    event === moduleOrEvent ||
                        // Event subscribed via module
                        event === moduleOrEvent.split('.').at(0) ||
                        // Event explicitly subscribed compared to module
                        event.split('.').at(0) === moduleOrEvent) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
    /**
     * Subscribes to event in the given context and channel.
     * @param {EventNames} event
     * @param {BrowsingContext.BrowsingContext | null} contextId
     * @param {BidiPlusChannel} 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(event, contextId, channel) {
        // All the subscriptions are handled on the top-level contexts.
        contextId = this.#browsingContextStorage.findTopLevelContextId(contextId);
        // Check if subscribed event is a whole module
        switch (event) {
            case ChromiumBidi.BiDiModule.BrowsingContext:
                return Object.values(ChromiumBidi.BrowsingContext.EventNames)
                    .map((specificEvent) => this.subscribe(specificEvent, contextId, channel))
                    .flat();
            case ChromiumBidi.BiDiModule.Log:
                return Object.values(ChromiumBidi.Log.EventNames)
                    .map((specificEvent) => this.subscribe(specificEvent, contextId, channel))
                    .flat();
            case ChromiumBidi.BiDiModule.Network:
                return Object.values(ChromiumBidi.Network.EventNames)
                    .map((specificEvent) => this.subscribe(specificEvent, contextId, channel))
                    .flat();
            case ChromiumBidi.BiDiModule.Script:
                return Object.values(ChromiumBidi.Script.EventNames)
                    .map((specificEvent) => this.subscribe(specificEvent, contextId, channel))
                    .flat();
            case ChromiumBidi.BiDiModule.Bluetooth:
                return Object.values(ChromiumBidi.Bluetooth.EventNames)
                    .map((specificEvent) => this.subscribe(specificEvent, contextId, channel))
                    .flat();
            default:
            // Intentionally left empty.
        }
        if (!this.#channelToContextToEventMap.has(channel)) {
            this.#channelToContextToEventMap.set(channel, new Map());
        }
        const contextToEventMap = this.#channelToContextToEventMap.get(channel);
        if (!contextToEventMap.has(contextId)) {
            contextToEventMap.set(contextId, new Map());
        }
        const eventMap = contextToEventMap.get(contextId);
        const affectedContextIds = (contextId === null
            ? this.#browsingContextStorage.getTopLevelContexts().map((c) => c.id)
            : [contextId])
            // There can be contexts that are already subscribed to the event. Do not include
            // them to the output.
            .filter((contextId) => !this.isSubscribedTo(event, contextId));
        if (!eventMap.has(event)) {
            // Add subscription only if it's not already subscribed.
            eventMap.set(event, this.#subscriptionPriority++);
        }
        return affectedContextIds.map((contextId) => ({
            event,
            contextId,
        }));
    }
    /**
     * Unsubscribes atomically from all events in the given contexts and channel.
     */
    unsubscribeAll(events, contextIds, channel) {
        // Assert all contexts are known.
        for (const contextId of contextIds) {
            if (contextId !== null) {
                this.#browsingContextStorage.getContext(contextId);
            }
        }
        const eventContextPairs = cartesianProduct(unrollEvents(events), contextIds);
        // Assert all unsubscriptions are valid.
        // If any of the unsubscriptions are invalid, do not unsubscribe from anything.
        eventContextPairs
            .map(([event, contextId]) => this.#checkUnsubscribe(event, contextId, channel))
            .forEach((unsubscribe) => unsubscribe());
    }
    /**
     * Unsubscribes from the event in the given context and channel.
     * Syntactic sugar for "unsubscribeAll".
     */
    unsubscribe(eventName, contextId, channel) {
        this.unsubscribeAll([eventName], [contextId], channel);
    }
    #checkUnsubscribe(event, contextId, channel) {
        // All the subscriptions are handled on the top-level contexts.
        contextId = this.#browsingContextStorage.findTopLevelContextId(contextId);
        if (!this.#channelToContextToEventMap.has(channel)) {
            throw new InvalidArgumentException(`Cannot unsubscribe from ${event}, ${contextId === null ? 'null' : contextId}. No subscription found.`);
        }
        const contextToEventMap = this.#channelToContextToEventMap.get(channel);
        if (!contextToEventMap.has(contextId)) {
            throw new InvalidArgumentException(`Cannot unsubscribe from ${event}, ${contextId === null ? 'null' : contextId}. No subscription found.`);
        }
        const eventMap = contextToEventMap.get(contextId);
        if (!eventMap.has(event)) {
            throw new InvalidArgumentException(`Cannot unsubscribe from ${event}, ${contextId === null ? 'null' : contextId}. No subscription found.`);
        }
        return () => {
            eventMap.delete(event);
            // Clean up maps if empty.
            if (eventMap.size === 0) {
                contextToEventMap.delete(event);
            }
            if (contextToEventMap.size === 0) {
                this.#channelToContextToEventMap.delete(channel);
            }
        };
    }
}
//# sourceMappingURL=SubscriptionManager.js.map