"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowsingContextProcessor = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const log_js_1 = require("../../../utils/log.js");
const WorkerRealm_js_1 = require("../script/WorkerRealm.js");
const BrowsingContextImpl_js_1 = require("./BrowsingContextImpl.js");
const CdpTarget_js_1 = require("./CdpTarget.js");
const cdpToBidiTargetTypes = {
    service_worker: 'service-worker',
    shared_worker: 'shared-worker',
    worker: 'dedicated-worker',
};
class BrowsingContextProcessor {
    #browserCdpClient;
    #cdpConnection;
    #selfTargetId;
    #eventManager;
    #browsingContextStorage;
    #networkStorage;
    #acceptInsecureCerts;
    #sharedIdWithFrame;
    #preloadScriptStorage;
    #realmStorage;
    #defaultUserContextId;
    #logger;
    constructor(cdpConnection, browserCdpClient, selfTargetId, eventManager, browsingContextStorage, realmStorage, networkStorage, preloadScriptStorage, acceptInsecureCerts, sharedIdWithFrame, defaultUserContextId, logger) {
        this.#acceptInsecureCerts = acceptInsecureCerts;
        this.#cdpConnection = cdpConnection;
        this.#browserCdpClient = browserCdpClient;
        this.#selfTargetId = selfTargetId;
        this.#eventManager = eventManager;
        this.#browsingContextStorage = browsingContextStorage;
        this.#preloadScriptStorage = preloadScriptStorage;
        this.#networkStorage = networkStorage;
        this.#realmStorage = realmStorage;
        this.#sharedIdWithFrame = sharedIdWithFrame;
        this.#defaultUserContextId = defaultUserContextId;
        this.#logger = logger;
        this.#setEventListeners(browserCdpClient);
    }
    getTree(params) {
        const resultContexts = params.root === undefined
            ? this.#browsingContextStorage.getTopLevelContexts()
            : [this.#browsingContextStorage.getContext(params.root)];
        return {
            contexts: resultContexts.map((c) => c.serializeToBidiValue(params.maxDepth ?? Number.MAX_VALUE)),
        };
    }
    async create(params) {
        let referenceContext;
        let userContext = params.userContext ?? 'default';
        if (params.referenceContext !== undefined) {
            referenceContext = this.#browsingContextStorage.getContext(params.referenceContext);
            if (!referenceContext.isTopLevelContext()) {
                throw new protocol_js_1.InvalidArgumentException(`referenceContext should be a top-level context`);
            }
            userContext = referenceContext.userContext;
        }
        let newWindow = false;
        switch (params.type) {
            case "tab" /* BrowsingContext.CreateType.Tab */:
                newWindow = false;
                break;
            case "window" /* BrowsingContext.CreateType.Window */:
                newWindow = true;
                break;
        }
        if (userContext !== 'default') {
            const existingContexts = this.#browsingContextStorage
                .getAllContexts()
                .filter((context) => context.userContext === userContext);
            if (!existingContexts.length) {
                // If there are no contexts in the given user context, we need to set
                // newWindow to true as newWindow=false will be rejected.
                newWindow = true;
            }
        }
        let result;
        try {
            result = await this.#browserCdpClient.sendCommand('Target.createTarget', {
                url: 'about:blank',
                newWindow,
                browserContextId: userContext === 'default' ? undefined : userContext,
            });
        }
        catch (err) {
            if (
            // See https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/devtools/protocol/target_handler.cc;l=90;drc=e80392ac11e48a691f4309964cab83a3a59e01c8
            err.message.startsWith('Failed to find browser context with id') ||
                // See https://source.chromium.org/chromium/chromium/src/+/main:headless/lib/browser/protocol/target_handler.cc;l=49;drc=e80392ac11e48a691f4309964cab83a3a59e01c8
                err.message === 'browserContextId') {
                throw new protocol_js_1.NoSuchUserContextException(`The context ${userContext} was not found`);
            }
            throw err;
        }
        // Wait for the new tab to be loaded to avoid race conditions in the
        // `browsingContext` events, when the `browsingContext.domContentLoaded` and
        // `browsingContext.load` events from the initial `about:blank` navigation
        // are emitted after the next navigation is started.
        // Details: https://github.com/web-platform-tests/wpt/issues/35846
        const contextId = result.targetId;
        const context = this.#browsingContextStorage.getContext(contextId);
        await context.lifecycleLoaded();
        return { context: context.id };
    }
    navigate(params) {
        const context = this.#browsingContextStorage.getContext(params.context);
        return context.navigate(params.url, params.wait ?? "none" /* BrowsingContext.ReadinessState.None */);
    }
    reload(params) {
        const context = this.#browsingContextStorage.getContext(params.context);
        return context.reload(params.ignoreCache ?? false, params.wait ?? "none" /* BrowsingContext.ReadinessState.None */);
    }
    async activate(params) {
        const context = this.#browsingContextStorage.getContext(params.context);
        if (!context.isTopLevelContext()) {
            throw new protocol_js_1.InvalidArgumentException('Activation is only supported on the top-level context');
        }
        await context.activate();
        return {};
    }
    async captureScreenshot(params) {
        const context = this.#browsingContextStorage.getContext(params.context);
        return await context.captureScreenshot(params);
    }
    async print(params) {
        const context = this.#browsingContextStorage.getContext(params.context);
        return await context.print(params);
    }
    async setViewport(params) {
        const context = this.#browsingContextStorage.getContext(params.context);
        if (!context.isTopLevelContext()) {
            throw new protocol_js_1.InvalidArgumentException('Emulating viewport is only supported on the top-level context');
        }
        await context.setViewport(params.viewport, params.devicePixelRatio);
        return {};
    }
    async traverseHistory(params) {
        const context = this.#browsingContextStorage.getContext(params.context);
        if (!context) {
            throw new protocol_js_1.InvalidArgumentException(`No browsing context with id ${params.context}`);
        }
        await context.traverseHistory(params.delta);
        return {};
    }
    async handleUserPrompt(params) {
        const context = this.#browsingContextStorage.getContext(params.context);
        await context.handleUserPrompt(params);
        return {};
    }
    async close(params) {
        const context = this.#browsingContextStorage.getContext(params.context);
        if (!context.isTopLevelContext()) {
            throw new protocol_js_1.InvalidArgumentException(`Non top-level browsing context ${context.id} cannot be closed.`);
        }
        try {
            const detachedFromTargetPromise = new Promise((resolve) => {
                const onContextDestroyed = (event) => {
                    if (event.targetId === params.context) {
                        this.#browserCdpClient.off('Target.detachedFromTarget', onContextDestroyed);
                        resolve();
                    }
                };
                this.#browserCdpClient.on('Target.detachedFromTarget', onContextDestroyed);
            });
            if (params.promptUnload) {
                await context.close();
            }
            else {
                await this.#browserCdpClient.sendCommand('Target.closeTarget', {
                    targetId: params.context,
                });
            }
            // Sometimes CDP command finishes before `detachedFromTarget` event,
            // sometimes after. Wait for the CDP command to be finished, and then wait
            // for `detachedFromTarget` if it hasn't emitted.
            await detachedFromTargetPromise;
        }
        catch (error) {
            // Swallow error that arise from the page being destroyed
            // Example is navigating to faulty SSL certificate
            if (!(error.code === -32000 /* CdpErrorConstants.GENERIC_ERROR */ &&
                error.message === 'Not attached to an active page')) {
                throw error;
            }
        }
        return {};
    }
    /**
     * This method is called for each CDP session, since this class is responsible
     * for creating and destroying all targets and browsing contexts.
     */
    #setEventListeners(cdpClient) {
        cdpClient.on('Target.attachedToTarget', (params) => {
            this.#handleAttachedToTargetEvent(params, cdpClient);
        });
        cdpClient.on('Target.detachedFromTarget', (params) => {
            this.#handleDetachedFromTargetEvent(params);
        });
        cdpClient.on('Target.targetInfoChanged', (params) => {
            this.#handleTargetInfoChangedEvent(params);
        });
        cdpClient.on('Inspector.targetCrashed', () => {
            this.#handleTargetCrashedEvent(cdpClient);
        });
        cdpClient.on('Page.frameAttached', (params) => {
            this.#handleFrameAttachedEvent(params);
        });
        cdpClient.on('Page.frameDetached', (params) => {
            this.#handleFrameDetachedEvent(params);
        });
    }
    #handleFrameAttachedEvent(params) {
        const parentBrowsingContext = this.#browsingContextStorage.findContext(params.parentFrameId);
        if (parentBrowsingContext !== undefined) {
            BrowsingContextImpl_js_1.BrowsingContextImpl.create(parentBrowsingContext.cdpTarget, this.#realmStorage, params.frameId, params.parentFrameId, parentBrowsingContext.userContext, this.#eventManager, this.#browsingContextStorage, this.#sharedIdWithFrame, this.#logger);
        }
    }
    #handleFrameDetachedEvent(params) {
        // In case of OOPiF no need in deleting BrowsingContext.
        if (params.reason === 'swap') {
            return;
        }
        this.#browsingContextStorage.findContext(params.frameId)?.dispose();
    }
    #handleAttachedToTargetEvent(params, parentSessionCdpClient) {
        const { sessionId, targetInfo } = params;
        const targetCdpClient = this.#cdpConnection.getCdpClient(sessionId);
        this.#logger?.(log_js_1.LogType.debugInfo, 'AttachedToTarget event received:', params);
        switch (targetInfo.type) {
            case 'page':
            case 'iframe': {
                if (targetInfo.targetId === this.#selfTargetId) {
                    break;
                }
                const cdpTarget = this.#createCdpTarget(targetCdpClient, targetInfo);
                const maybeContext = this.#browsingContextStorage.findContext(targetInfo.targetId);
                if (maybeContext) {
                    // OOPiF.
                    maybeContext.updateCdpTarget(cdpTarget);
                }
                else {
                    // New context.
                    BrowsingContextImpl_js_1.BrowsingContextImpl.create(cdpTarget, this.#realmStorage, targetInfo.targetId, null, targetInfo.browserContextId &&
                        targetInfo.browserContextId !== this.#defaultUserContextId
                        ? targetInfo.browserContextId
                        : 'default', this.#eventManager, this.#browsingContextStorage, this.#sharedIdWithFrame, this.#logger);
                }
                return;
            }
            case 'service_worker':
            case 'worker': {
                const realm = this.#realmStorage.findRealm({
                    cdpSessionId: parentSessionCdpClient.sessionId,
                });
                // If there is no browsing context, this worker is already terminated.
                if (!realm) {
                    break;
                }
                const cdpTarget = this.#createCdpTarget(targetCdpClient, targetInfo);
                this.#handleWorkerTarget(cdpToBidiTargetTypes[targetInfo.type], cdpTarget, realm);
                return;
            }
            // In CDP, we only emit shared workers on the browser and not the set of
            // frames that use the shared worker. If we change this in the future to
            // behave like service workers (emits on both browser and frame targets),
            // we can remove this block and merge service workers with the above one.
            case 'shared_worker': {
                const cdpTarget = this.#createCdpTarget(targetCdpClient, targetInfo);
                this.#handleWorkerTarget(cdpToBidiTargetTypes[targetInfo.type], cdpTarget);
                return;
            }
        }
        // DevTools or some other not supported by BiDi target. Just release
        // debugger and ignore them.
        targetCdpClient
            .sendCommand('Runtime.runIfWaitingForDebugger')
            .then(() => parentSessionCdpClient.sendCommand('Target.detachFromTarget', params))
            .catch((error) => this.#logger?.(log_js_1.LogType.debugError, error));
    }
    #createCdpTarget(targetCdpClient, targetInfo) {
        this.#setEventListeners(targetCdpClient);
        const target = CdpTarget_js_1.CdpTarget.create(targetInfo.targetId, targetCdpClient, this.#browserCdpClient, this.#realmStorage, this.#eventManager, this.#preloadScriptStorage, this.#networkStorage, this.#acceptInsecureCerts, this.#logger);
        this.#networkStorage.onCdpTargetCreated(target);
        return target;
    }
    #workers = new Map();
    #handleWorkerTarget(realmType, cdpTarget, ownerRealm) {
        cdpTarget.cdpClient.on('Runtime.executionContextCreated', (params) => {
            const { uniqueId, id, origin } = params.context;
            const workerRealm = new WorkerRealm_js_1.WorkerRealm(cdpTarget.cdpClient, this.#eventManager, id, this.#logger, (0, BrowsingContextImpl_js_1.serializeOrigin)(origin), ownerRealm ? [ownerRealm] : [], uniqueId, this.#realmStorage, realmType);
            this.#workers.set(cdpTarget.cdpSessionId, workerRealm);
        });
    }
    #handleDetachedFromTargetEvent(params) {
        const context = this.#browsingContextStorage.findContextBySession(params.sessionId);
        if (context) {
            context.dispose();
            this.#preloadScriptStorage
                .find({ targetId: context.id })
                .map((preloadScript) => preloadScript.dispose(context.id));
            return;
        }
        const worker = this.#workers.get(params.sessionId);
        if (worker) {
            this.#realmStorage.deleteRealms({
                cdpSessionId: worker.cdpClient.sessionId,
            });
        }
    }
    #handleTargetInfoChangedEvent(params) {
        const context = this.#browsingContextStorage.findContext(params.targetInfo.targetId);
        if (context) {
            context.onTargetInfoChanged(params);
        }
    }
    #handleTargetCrashedEvent(cdpClient) {
        // This is primarily used for service and shared workers. CDP tends to not
        // signal they closed gracefully and instead says they crashed to signal
        // they are closed.
        const realms = this.#realmStorage.findRealms({
            cdpSessionId: cdpClient.sessionId,
        });
        for (const realm of realms) {
            realm.dispose();
        }
    }
}
exports.BrowsingContextProcessor = BrowsingContextProcessor;
//# sourceMappingURL=BrowsingContextProcessor.js.map