"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.serializeOrigin = exports.BrowsingContextImpl = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const assert_js_1 = require("../../../utils/assert.js");
const Deferred_js_1 = require("../../../utils/Deferred.js");
const log_js_1 = require("../../../utils/log.js");
const unitConversions_js_1 = require("../../../utils/unitConversions.js");
const WindowRealm_js_1 = require("../script/WindowRealm.js");
class BrowsingContextImpl {
    static LOGGER_PREFIX = `${log_js_1.LogType.debug}:browsingContext`;
    /** The ID of this browsing context. */
    #id;
    userContext;
    /**
     * The ID of the parent browsing context.
     * If null, this is a top-level context.
     */
    #parentId;
    /** Direct children browsing contexts. */
    #children = new Set();
    #browsingContextStorage;
    #lifecycle = {
        DOMContentLoaded: new Deferred_js_1.Deferred(),
        load: new Deferred_js_1.Deferred(),
    };
    #navigation = {
        withinDocument: new Deferred_js_1.Deferred(),
    };
    #url = 'about:blank';
    #eventManager;
    #realmStorage;
    #loaderId;
    #cdpTarget;
    #maybeDefaultRealm;
    #sharedIdWithFrame;
    #logger;
    constructor(cdpTarget, realmStorage, id, parentId, userContext, eventManager, browsingContextStorage, sharedIdWithFrame, logger) {
        this.#cdpTarget = cdpTarget;
        this.#realmStorage = realmStorage;
        this.#id = id;
        this.#parentId = parentId;
        this.userContext = userContext;
        this.#eventManager = eventManager;
        this.#browsingContextStorage = browsingContextStorage;
        this.#sharedIdWithFrame = sharedIdWithFrame;
        this.#logger = logger;
    }
    static create(cdpTarget, realmStorage, id, parentId, userContext, eventManager, browsingContextStorage, sharedIdWithFrame, logger) {
        const context = new BrowsingContextImpl(cdpTarget, realmStorage, id, parentId, userContext, eventManager, browsingContextStorage, sharedIdWithFrame, logger);
        context.#initListeners();
        browsingContextStorage.addContext(context);
        if (!context.isTopLevelContext()) {
            context.parent.addChild(context.id);
        }
        eventManager.registerEvent({
            type: 'event',
            method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.ContextCreated,
            params: context.serializeToBidiValue(),
        }, context.id);
        return context;
    }
    static getTimestamp() {
        // `timestamp` from the event is MonotonicTime, not real time, so
        // the best Mapper can do is to set the timestamp to the epoch time
        // of the event arrived.
        // https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-MonotonicTime
        return new Date().getTime();
    }
    /**
     * @see https://html.spec.whatwg.org/multipage/document-sequences.html#navigable
     */
    get navigableId() {
        return this.#loaderId;
    }
    dispose() {
        this.#deleteAllChildren();
        this.#realmStorage.deleteRealms({
            browsingContextId: this.id,
        });
        // Remove context from the parent.
        if (!this.isTopLevelContext()) {
            this.parent.#children.delete(this.id);
        }
        // Fail all ongoing navigations.
        this.#failLifecycleIfNotFinished();
        this.#eventManager.registerEvent({
            type: 'event',
            method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.ContextDestroyed,
            params: this.serializeToBidiValue(),
        }, this.id);
        this.#browsingContextStorage.deleteContextById(this.id);
    }
    /** Returns the ID of this context. */
    get id() {
        return this.#id;
    }
    /** Returns the parent context ID. */
    get parentId() {
        return this.#parentId;
    }
    /** Returns the parent context. */
    get parent() {
        if (this.parentId === null) {
            return null;
        }
        return this.#browsingContextStorage.getContext(this.parentId);
    }
    /** Returns all direct children contexts. */
    get directChildren() {
        return [...this.#children].map((id) => this.#browsingContextStorage.getContext(id));
    }
    /** Returns all children contexts, flattened. */
    get allChildren() {
        const children = this.directChildren;
        return children.concat(...children.map((child) => child.allChildren));
    }
    /**
     * Returns true if this is a top-level context.
     * This is the case whenever the parent context ID is null.
     */
    isTopLevelContext() {
        return this.#parentId === null;
    }
    get top() {
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        let topContext = this;
        let parent = topContext.parent;
        while (parent) {
            topContext = parent;
            parent = topContext.parent;
        }
        return topContext;
    }
    addChild(childId) {
        this.#children.add(childId);
    }
    #deleteAllChildren() {
        this.directChildren.map((child) => child.dispose());
    }
    get #defaultRealm() {
        (0, assert_js_1.assert)(this.#maybeDefaultRealm, `No default realm for browsing context ${this.#id}`);
        return this.#maybeDefaultRealm;
    }
    get cdpTarget() {
        return this.#cdpTarget;
    }
    updateCdpTarget(cdpTarget) {
        this.#cdpTarget = cdpTarget;
        this.#initListeners();
    }
    get url() {
        return this.#url;
    }
    async lifecycleLoaded() {
        await this.#lifecycle.load;
    }
    async targetUnblockedOrThrow() {
        const result = await this.#cdpTarget.unblocked;
        if (result.kind === 'error') {
            throw result.error;
        }
    }
    async getOrCreateSandbox(sandbox) {
        if (sandbox === undefined || sandbox === '') {
            return this.#defaultRealm;
        }
        let maybeSandboxes = this.#realmStorage.findRealms({
            browsingContextId: this.id,
            sandbox,
        });
        if (maybeSandboxes.length === 0) {
            await this.#cdpTarget.cdpClient.sendCommand('Page.createIsolatedWorld', {
                frameId: this.id,
                worldName: sandbox,
            });
            // `Runtime.executionContextCreated` should be emitted by the time the
            // previous command is done.
            maybeSandboxes = this.#realmStorage.findRealms({
                browsingContextId: this.id,
                sandbox,
            });
            (0, assert_js_1.assert)(maybeSandboxes.length !== 0);
        }
        // It's possible for more than one sandbox to be created due to provisional
        // frames. In this case, it's always the first one (i.e. the oldest one)
        // that is more relevant since the user may have set that one up already
        // through evaluation.
        return maybeSandboxes[0];
    }
    serializeToBidiValue(maxDepth = 0, addParentField = true) {
        return {
            context: this.#id,
            url: this.url,
            userContext: this.userContext,
            children: maxDepth > 0
                ? this.directChildren.map((c) => c.serializeToBidiValue(maxDepth - 1, false))
                : null,
            ...(addParentField ? { parent: this.#parentId } : {}),
        };
    }
    onTargetInfoChanged(params) {
        this.#url = params.targetInfo.url;
    }
    #initListeners() {
        this.#cdpTarget.cdpClient.on('Page.frameNavigated', (params) => {
            if (this.id !== params.frame.id) {
                return;
            }
            this.#url = params.frame.url + (params.frame.urlFragment ?? '');
            // At the point the page is initialized, all the nested iframes from the
            // previous page are detached and realms are destroyed.
            // Remove children from context.
            this.#deleteAllChildren();
        });
        this.#cdpTarget.cdpClient.on('Page.navigatedWithinDocument', (params) => {
            if (this.id !== params.frameId) {
                return;
            }
            const timestamp = BrowsingContextImpl.getTimestamp();
            this.#url = params.url;
            this.#navigation.withinDocument.resolve(params);
            this.#eventManager.registerEvent({
                type: 'event',
                method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.FragmentNavigated,
                params: {
                    context: this.id,
                    navigation: null,
                    timestamp,
                    url: this.#url,
                },
            }, this.id);
        });
        this.#cdpTarget.cdpClient.on('Page.frameStartedLoading', (params) => {
            if (this.id !== params.frameId) {
                return;
            }
            this.#eventManager.registerEvent({
                type: 'event',
                method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.NavigationStarted,
                params: {
                    context: this.id,
                    navigation: null,
                    timestamp: BrowsingContextImpl.getTimestamp(),
                    url: '',
                },
            }, this.id);
        });
        this.#cdpTarget.cdpClient.on('Page.lifecycleEvent', (params) => {
            if (this.id !== params.frameId) {
                return;
            }
            if (params.name === 'init') {
                this.#documentChanged(params.loaderId);
                return;
            }
            if (params.name === 'commit') {
                this.#loaderId = params.loaderId;
                return;
            }
            // Ignore event from not current navigation.
            if (params.loaderId !== this.#loaderId) {
                return;
            }
            const timestamp = BrowsingContextImpl.getTimestamp();
            switch (params.name) {
                case 'DOMContentLoaded':
                    this.#eventManager.registerEvent({
                        type: 'event',
                        method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.DomContentLoaded,
                        params: {
                            context: this.id,
                            navigation: this.#loaderId ?? null,
                            timestamp,
                            url: this.#url,
                        },
                    }, this.id);
                    this.#lifecycle.DOMContentLoaded.resolve(params);
                    break;
                case 'load':
                    this.#eventManager.registerEvent({
                        type: 'event',
                        method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.Load,
                        params: {
                            context: this.id,
                            navigation: this.#loaderId ?? null,
                            timestamp,
                            url: this.#url,
                        },
                    }, this.id);
                    this.#lifecycle.load.resolve(params);
                    break;
            }
        });
        this.#cdpTarget.cdpClient.on('Runtime.executionContextCreated', (params) => {
            const { auxData, name, uniqueId, id } = params.context;
            if (!auxData || auxData.frameId !== this.id) {
                return;
            }
            let origin;
            let sandbox;
            // Only these execution contexts are supported for now.
            switch (auxData.type) {
                case 'isolated':
                    sandbox = name;
                    // Sandbox should have the same origin as the context itself, but in CDP
                    // it has an empty one.
                    origin = this.#defaultRealm.origin;
                    break;
                case 'default':
                    origin = serializeOrigin(params.context.origin);
                    break;
                default:
                    return;
            }
            const realm = new WindowRealm_js_1.WindowRealm(this.id, this.#browsingContextStorage, this.#cdpTarget.cdpClient, this.#eventManager, id, this.#logger, origin, uniqueId, this.#realmStorage, sandbox, this.#sharedIdWithFrame);
            if (auxData.isDefault) {
                this.#maybeDefaultRealm = realm;
                // Initialize ChannelProxy listeners for all the channels of all the
                // preload scripts related to this BrowsingContext.
                // TODO: extend for not default realms by the sandbox name.
                void Promise.all(this.#cdpTarget
                    .getChannels()
                    .map((channel) => channel.startListenerFromWindow(realm, this.#eventManager)));
            }
        });
        this.#cdpTarget.cdpClient.on('Runtime.executionContextDestroyed', (params) => {
            this.#realmStorage.deleteRealms({
                cdpSessionId: this.#cdpTarget.cdpSessionId,
                executionContextId: params.executionContextId,
            });
        });
        this.#cdpTarget.cdpClient.on('Runtime.executionContextsCleared', () => {
            this.#realmStorage.deleteRealms({
                cdpSessionId: this.#cdpTarget.cdpSessionId,
            });
        });
        this.#cdpTarget.cdpClient.on('Page.javascriptDialogClosed', (params) => {
            const accepted = params.result;
            this.#eventManager.registerEvent({
                type: 'event',
                method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.UserPromptClosed,
                params: {
                    context: this.id,
                    accepted,
                    userText: accepted && params.userInput ? params.userInput : undefined,
                },
            }, this.id);
        });
        this.#cdpTarget.cdpClient.on('Page.javascriptDialogOpening', (params) => {
            this.#eventManager.registerEvent({
                type: 'event',
                method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.UserPromptOpened,
                params: {
                    context: this.id,
                    type: params.type,
                    message: params.message,
                    // Don't set the value if empty string
                    defaultValue: params.defaultPrompt || undefined,
                },
            }, this.id);
        });
    }
    #documentChanged(loaderId) {
        // Same document navigation.
        if (loaderId === undefined || this.#loaderId === loaderId) {
            if (this.#navigation.withinDocument.isFinished) {
                this.#navigation.withinDocument =
                    new Deferred_js_1.Deferred();
            }
            else {
                this.#logger?.(BrowsingContextImpl.LOGGER_PREFIX, 'Document changed (navigatedWithinDocument)');
            }
            return;
        }
        this.#resetLifecycleIfFinished();
        this.#loaderId = loaderId;
    }
    #resetLifecycleIfFinished() {
        if (this.#lifecycle.DOMContentLoaded.isFinished) {
            this.#lifecycle.DOMContentLoaded =
                new Deferred_js_1.Deferred();
        }
        else {
            this.#logger?.(BrowsingContextImpl.LOGGER_PREFIX, 'Document changed (DOMContentLoaded)');
        }
        if (this.#lifecycle.load.isFinished) {
            this.#lifecycle.load = new Deferred_js_1.Deferred();
        }
        else {
            this.#logger?.(BrowsingContextImpl.LOGGER_PREFIX, 'Document changed (load)');
        }
    }
    #failLifecycleIfNotFinished() {
        if (!this.#lifecycle.DOMContentLoaded.isFinished) {
            this.#lifecycle.DOMContentLoaded.reject(new protocol_js_1.UnknownErrorException('navigation canceled'));
        }
        if (!this.#lifecycle.load.isFinished) {
            this.#lifecycle.load.reject(new protocol_js_1.UnknownErrorException('navigation canceled'));
        }
    }
    async navigate(url, wait) {
        try {
            new URL(url);
        }
        catch {
            throw new protocol_js_1.InvalidArgumentException(`Invalid URL: ${url}`);
        }
        await this.targetUnblockedOrThrow();
        // TODO: handle loading errors.
        const cdpNavigateResult = await this.#cdpTarget.cdpClient.sendCommand('Page.navigate', {
            url,
            frameId: this.id,
        });
        if (cdpNavigateResult.errorText) {
            throw new protocol_js_1.UnknownErrorException(cdpNavigateResult.errorText);
        }
        this.#documentChanged(cdpNavigateResult.loaderId);
        switch (wait) {
            case "none" /* BrowsingContext.ReadinessState.None */:
                break;
            case "interactive" /* BrowsingContext.ReadinessState.Interactive */:
                // No `loaderId` means same-document navigation.
                if (cdpNavigateResult.loaderId === undefined) {
                    await this.#navigation.withinDocument;
                }
                else {
                    await this.#lifecycle.DOMContentLoaded;
                }
                break;
            case "complete" /* BrowsingContext.ReadinessState.Complete */:
                // No `loaderId` means same-document navigation.
                if (cdpNavigateResult.loaderId === undefined) {
                    await this.#navigation.withinDocument;
                }
                else {
                    await this.#lifecycle.load;
                }
                break;
        }
        return {
            navigation: cdpNavigateResult.loaderId ?? null,
            // Url can change due to redirect get the latest one.
            url: wait === "none" /* BrowsingContext.ReadinessState.None */ ? url : this.#url,
        };
    }
    async reload(ignoreCache, wait) {
        await this.targetUnblockedOrThrow();
        await this.#cdpTarget.cdpClient.sendCommand('Page.reload', {
            ignoreCache,
        });
        this.#resetLifecycleIfFinished();
        switch (wait) {
            case "none" /* BrowsingContext.ReadinessState.None */:
                break;
            case "interactive" /* BrowsingContext.ReadinessState.Interactive */:
                await this.#lifecycle.DOMContentLoaded;
                break;
            case "complete" /* BrowsingContext.ReadinessState.Complete */:
                await this.#lifecycle.load;
                break;
        }
        return {
            navigation: wait === "none" /* BrowsingContext.ReadinessState.None */
                ? null
                : this.navigableId ?? null,
            url: this.url,
        };
    }
    async setViewport(viewport, devicePixelRatio) {
        if (viewport === null && devicePixelRatio === null) {
            await this.#cdpTarget.cdpClient.sendCommand('Emulation.clearDeviceMetricsOverride');
        }
        else {
            try {
                await this.#cdpTarget.cdpClient.sendCommand('Emulation.setDeviceMetricsOverride', {
                    width: viewport ? viewport.width : 0,
                    height: viewport ? viewport.height : 0,
                    deviceScaleFactor: devicePixelRatio ? devicePixelRatio : 0,
                    mobile: false,
                    dontSetVisibleSize: true,
                });
            }
            catch (err) {
                if (err.message.startsWith(
                // https://crsrc.org/c/content/browser/devtools/protocol/emulation_handler.cc;l=257;drc=2f6eee84cf98d4227e7c41718dd71b82f26d90ff
                'Width and height values must be positive')) {
                    throw new protocol_js_1.UnsupportedOperationException('Provided viewport dimensions are not supported');
                }
                throw err;
            }
        }
    }
    async handleUserPrompt(params) {
        await this.#cdpTarget.cdpClient.sendCommand('Page.handleJavaScriptDialog', {
            accept: params.accept ?? true,
            promptText: params.userText,
        });
    }
    async activate() {
        await this.#cdpTarget.cdpClient.sendCommand('Page.bringToFront');
    }
    async captureScreenshot(params) {
        if (!this.isTopLevelContext()) {
            throw new protocol_js_1.UnsupportedOperationException(`Non-top-level 'context' (${params.context}) is currently not supported`);
        }
        const formatParameters = getImageFormatParameters(params);
        // XXX: Focus the original tab after the screenshot is taken.
        // This is needed because the screenshot gets blocked until the active tab gets focus.
        await this.#cdpTarget.cdpClient.sendCommand('Page.bringToFront');
        let captureBeyondViewport = false;
        let script;
        params.origin ??= 'viewport';
        switch (params.origin) {
            case 'document': {
                script = String(() => {
                    const element = document.documentElement;
                    return {
                        x: 0,
                        y: 0,
                        width: element.scrollWidth,
                        height: element.scrollHeight,
                    };
                });
                captureBeyondViewport = true;
                break;
            }
            case 'viewport': {
                script = String(() => {
                    const viewport = window.visualViewport;
                    return {
                        x: viewport.pageLeft,
                        y: viewport.pageTop,
                        width: viewport.width,
                        height: viewport.height,
                    };
                });
                break;
            }
        }
        const realm = await this.getOrCreateSandbox(undefined);
        const originResult = await realm.callFunction(script, { type: 'undefined' }, [], false, "none" /* Script.ResultOwnership.None */, {}, false);
        (0, assert_js_1.assert)(originResult.type === 'success');
        const origin = deserializeDOMRect(originResult.result);
        (0, assert_js_1.assert)(origin);
        const rect = params.clip
            ? getIntersectionRect(await this.#parseRect(params.clip), origin)
            : origin;
        if (rect.width === 0 || rect.height === 0) {
            throw new protocol_js_1.UnableToCaptureScreenException(`Unable to capture screenshot with zero dimensions: width=${rect.width}, height=${rect.height}`);
        }
        return await this.#cdpTarget.cdpClient.sendCommand('Page.captureScreenshot', {
            clip: { ...rect, scale: 1.0 },
            ...formatParameters,
            captureBeyondViewport,
        });
    }
    async print(params) {
        const cdpParams = {};
        if (params.background !== undefined) {
            cdpParams.printBackground = params.background;
        }
        if (params.margin?.bottom !== undefined) {
            cdpParams.marginBottom = (0, unitConversions_js_1.inchesFromCm)(params.margin.bottom);
        }
        if (params.margin?.left !== undefined) {
            cdpParams.marginLeft = (0, unitConversions_js_1.inchesFromCm)(params.margin.left);
        }
        if (params.margin?.right !== undefined) {
            cdpParams.marginRight = (0, unitConversions_js_1.inchesFromCm)(params.margin.right);
        }
        if (params.margin?.top !== undefined) {
            cdpParams.marginTop = (0, unitConversions_js_1.inchesFromCm)(params.margin.top);
        }
        if (params.orientation !== undefined) {
            cdpParams.landscape = params.orientation === 'landscape';
        }
        if (params.page?.height !== undefined) {
            cdpParams.paperHeight = (0, unitConversions_js_1.inchesFromCm)(params.page.height);
        }
        if (params.page?.width !== undefined) {
            cdpParams.paperWidth = (0, unitConversions_js_1.inchesFromCm)(params.page.width);
        }
        if (params.pageRanges !== undefined) {
            for (const range of params.pageRanges) {
                if (typeof range === 'number') {
                    continue;
                }
                const rangeParts = range.split('-');
                if (rangeParts.length < 1 || rangeParts.length > 2) {
                    throw new protocol_js_1.InvalidArgumentException(`Invalid page range: ${range} is not a valid integer range.`);
                }
                if (rangeParts.length === 1) {
                    void parseInteger(rangeParts[0] ?? '');
                    continue;
                }
                let lowerBound;
                let upperBound;
                const [rangeLowerPart = '', rangeUpperPart = ''] = rangeParts;
                if (rangeLowerPart === '') {
                    lowerBound = 1;
                }
                else {
                    lowerBound = parseInteger(rangeLowerPart);
                }
                if (rangeUpperPart === '') {
                    upperBound = Number.MAX_SAFE_INTEGER;
                }
                else {
                    upperBound = parseInteger(rangeUpperPart);
                }
                if (lowerBound > upperBound) {
                    throw new protocol_js_1.InvalidArgumentException(`Invalid page range: ${rangeLowerPart} > ${rangeUpperPart}`);
                }
            }
            cdpParams.pageRanges = params.pageRanges.join(',');
        }
        if (params.scale !== undefined) {
            cdpParams.scale = params.scale;
        }
        if (params.shrinkToFit !== undefined) {
            cdpParams.preferCSSPageSize = !params.shrinkToFit;
        }
        try {
            const result = await this.#cdpTarget.cdpClient.sendCommand('Page.printToPDF', cdpParams);
            return {
                data: result.data,
            };
        }
        catch (error) {
            // Effectively zero dimensions.
            if (error.message ===
                'invalid print parameters: content area is empty') {
                throw new protocol_js_1.UnsupportedOperationException(error.message);
            }
            throw error;
        }
    }
    /**
     * See
     * https://w3c.github.io/webdriver-bidi/#:~:text=If%20command%20parameters%20contains%20%22clip%22%3A
     */
    async #parseRect(clip) {
        switch (clip.type) {
            case 'box':
                return { x: clip.x, y: clip.y, width: clip.width, height: clip.height };
            case 'element': {
                // TODO: #1213: Use custom sandbox specifically for Chromium BiDi
                const sandbox = await this.getOrCreateSandbox(undefined);
                const result = await sandbox.callFunction(String((element) => {
                    return element instanceof Element;
                }), { type: 'undefined' }, [clip.element], false, "none" /* Script.ResultOwnership.None */, {});
                if (result.type === 'exception') {
                    throw new protocol_js_1.NoSuchElementException(`Element '${clip.element.sharedId}' was not found`);
                }
                (0, assert_js_1.assert)(result.result.type === 'boolean');
                if (!result.result.value) {
                    throw new protocol_js_1.NoSuchElementException(`Node '${clip.element.sharedId}' is not an Element`);
                }
                {
                    const result = await sandbox.callFunction(String((element) => {
                        const rect = element.getBoundingClientRect();
                        return {
                            x: rect.x,
                            y: rect.y,
                            height: rect.height,
                            width: rect.width,
                        };
                    }), { type: 'undefined' }, [clip.element], false, "none" /* Script.ResultOwnership.None */, {});
                    (0, assert_js_1.assert)(result.type === 'success');
                    const rect = deserializeDOMRect(result.result);
                    if (!rect) {
                        throw new protocol_js_1.UnableToCaptureScreenException(`Could not get bounding box for Element '${clip.element.sharedId}'`);
                    }
                    return rect;
                }
            }
        }
    }
    async close() {
        await this.#cdpTarget.cdpClient.sendCommand('Page.close');
    }
    async traverseHistory(delta) {
        if (delta === 0) {
            return;
        }
        const history = await this.#cdpTarget.cdpClient.sendCommand('Page.getNavigationHistory');
        const entry = history.entries[history.currentIndex + delta];
        if (!entry) {
            throw new protocol_js_1.NoSuchHistoryEntryException(`No history entry at delta ${delta}`);
        }
        await this.#cdpTarget.cdpClient.sendCommand('Page.navigateToHistoryEntry', {
            entryId: entry.id,
        });
    }
}
exports.BrowsingContextImpl = BrowsingContextImpl;
function serializeOrigin(origin) {
    // https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin
    if (['://', ''].includes(origin)) {
        origin = 'null';
    }
    return origin;
}
exports.serializeOrigin = serializeOrigin;
function getImageFormatParameters(params) {
    const { quality, type } = params.format ?? {
        type: 'image/png',
    };
    switch (type) {
        case 'image/png': {
            return { format: 'png' };
        }
        case 'image/jpeg': {
            return {
                format: 'jpeg',
                ...(quality === undefined ? {} : { quality: Math.round(quality * 100) }),
            };
        }
        case 'image/webp': {
            return {
                format: 'webp',
                ...(quality === undefined ? {} : { quality: Math.round(quality * 100) }),
            };
        }
    }
    throw new protocol_js_1.InvalidArgumentException(`Image format '${type}' is not a supported format`);
}
function deserializeDOMRect(result) {
    if (result.type !== 'object' || result.value === undefined) {
        return;
    }
    const x = result.value.find(([key]) => {
        return key === 'x';
    })?.[1];
    const y = result.value.find(([key]) => {
        return key === 'y';
    })?.[1];
    const height = result.value.find(([key]) => {
        return key === 'height';
    })?.[1];
    const width = result.value.find(([key]) => {
        return key === 'width';
    })?.[1];
    if (x?.type !== 'number' ||
        y?.type !== 'number' ||
        height?.type !== 'number' ||
        width?.type !== 'number') {
        return;
    }
    return {
        x: x.value,
        y: y.value,
        width: width.value,
        height: height.value,
    };
}
/** @see https://w3c.github.io/webdriver-bidi/#normalize-rect */
function normalizeRect(box) {
    return {
        ...(box.width < 0
            ? {
                x: box.x + box.width,
                width: -box.width,
            }
            : {
                x: box.x,
                width: box.width,
            }),
        ...(box.height < 0
            ? {
                y: box.y + box.height,
                height: -box.height,
            }
            : {
                y: box.y,
                height: box.height,
            }),
    };
}
/** @see https://w3c.github.io/webdriver-bidi/#rectangle-intersection */
function getIntersectionRect(first, second) {
    first = normalizeRect(first);
    second = normalizeRect(second);
    const x = Math.max(first.x, second.x);
    const y = Math.max(first.y, second.y);
    return {
        x,
        y,
        width: Math.max(Math.min(first.x + first.width, second.x + second.width) - x, 0),
        height: Math.max(Math.min(first.y + first.height, second.y + second.height) - y, 0),
    };
}
function parseInteger(value) {
    value = value.trim();
    if (!/^[0-9]+$/.test(value)) {
        throw new protocol_js_1.InvalidArgumentException(`Invalid integer: ${value}`);
    }
    return parseInt(value);
}
//# sourceMappingURL=BrowsingContextImpl.js.map