/*
 *  Copyright 2024 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, } from '../../../protocol/protocol.js';
import { Deferred } from '../../../utils/Deferred.js';
import { LogType } from '../../../utils/log.js';
import { getTimestamp } from '../../../utils/time.js';
import { urlMatchesAboutBlank } from '../../../utils/UrlHelpers.js';
import { uuidv4 } from '../../../utils/uuid.js';
export class NavigationResult {
    eventName;
    message;
    constructor(eventName, message) {
        this.eventName = eventName;
        this.message = message;
    }
}
class NavigationState {
    navigationId = uuidv4();
    #browsingContextId;
    #started = false;
    #finished = new Deferred();
    url;
    loaderId;
    #isInitial;
    #eventManager;
    #navigated = false;
    get finished() {
        return this.#finished;
    }
    constructor(url, browsingContextId, isInitial, eventManager) {
        this.#browsingContextId = browsingContextId;
        this.url = url;
        this.#isInitial = isInitial;
        this.#eventManager = eventManager;
    }
    navigationInfo() {
        return {
            context: this.#browsingContextId,
            navigation: this.navigationId,
            timestamp: getTimestamp(),
            url: this.url,
        };
    }
    start() {
        if (!this.#isInitial && !this.#started) {
            this.#eventManager.registerEvent({
                type: 'event',
                method: ChromiumBidi.BrowsingContext.EventNames.NavigationStarted,
                params: this.navigationInfo(),
            }, this.#browsingContextId);
        }
        this.#started = true;
    }
    #finish(navigationResult) {
        this.#started = true;
        if (!this.#isInitial &&
            !this.#finished.isFinished &&
            navigationResult.eventName !== "browsingContext.load" /* NavigationEventName.Load */) {
            this.#eventManager.registerEvent({
                type: 'event',
                method: navigationResult.eventName,
                params: this.navigationInfo(),
            }, this.#browsingContextId);
        }
        this.#finished.resolve(navigationResult);
    }
    frameNavigated() {
        this.#navigated = true;
    }
    fragmentNavigated() {
        this.#navigated = true;
        this.#finish(new NavigationResult("browsingContext.fragmentNavigated" /* NavigationEventName.FragmentNavigated */));
    }
    load() {
        this.#finish(new NavigationResult("browsingContext.load" /* NavigationEventName.Load */));
    }
    fail(message) {
        this.#finish(new NavigationResult(this.#navigated
            ? "browsingContext.navigationAborted" /* NavigationEventName.NavigationAborted */
            : "browsingContext.navigationFailed" /* NavigationEventName.NavigationFailed */, message));
    }
}
/**
 * Keeps track of navigations. Details: http://go/webdriver:bidi-navigation
 */
export class NavigationTracker {
    #eventManager;
    #logger;
    #loaderIdToNavigationsMap = new Map();
    #browsingContextId;
    #currentNavigation;
    // When a new navigation is started via `BrowsingContext.navigate` with `wait` set to
    // `None`, the command result should have `navigation` value, but mapper does not have
    // it yet. This value will be set to `navigationId` after next .
    #pendingNavigation;
    // Flags if the initial navigation to `about:blank` is in progress.
    #isInitialNavigation = true;
    navigation = {
        withinDocument: new Deferred(),
    };
    constructor(url, browsingContextId, eventManager, logger) {
        this.#browsingContextId = browsingContextId;
        this.#eventManager = eventManager;
        this.#logger = logger;
        this.#isInitialNavigation = true;
        this.#currentNavigation = new NavigationState(url, browsingContextId, urlMatchesAboutBlank(url), this.#eventManager);
    }
    /**
     * Returns current started ongoing navigation. It can be either a started pending
     * navigation, or one is already navigated.
     */
    get currentNavigationId() {
        if (this.#pendingNavigation?.loaderId !== undefined) {
            return this.#pendingNavigation.navigationId;
        }
        return this.#currentNavigation.navigationId;
    }
    /**
     * Flags if the current navigation relates to the initial to `about:blank` navigation.
     */
    get isInitialNavigation() {
        return this.#isInitialNavigation;
    }
    /**
     * Url of the last navigated navigation.
     */
    get url() {
        return this.#currentNavigation.url;
    }
    /**
     * Creates a pending navigation e.g. when navigation command is called. Required to
     * provide navigation id before the actual navigation is started. It will be used when
     * navigation started. Can be aborted, failed, fragment navigated, or became a current
     * navigation.
     */
    createPendingNavigation(url, canBeInitialNavigation = false) {
        this.#logger?.(LogType.debug, 'createCommandNavigation');
        this.#isInitialNavigation =
            canBeInitialNavigation &&
                this.#isInitialNavigation &&
                urlMatchesAboutBlank(url);
        this.#pendingNavigation?.fail('navigation canceled by concurrent navigation');
        const navigation = new NavigationState(url, this.#browsingContextId, this.#isInitialNavigation, this.#eventManager);
        this.#pendingNavigation = navigation;
        return navigation;
    }
    dispose() {
        this.#pendingNavigation?.fail('navigation canceled by context disposal');
        this.#currentNavigation.fail('navigation canceled by context disposal');
    }
    // Update the current url.
    onTargetInfoChanged(url) {
        this.#logger?.(LogType.debug, `onTargetInfoChanged ${url}`);
        this.#currentNavigation.url = url;
    }
    #getNavigationForFrameNavigated(url, loaderId) {
        if (this.#loaderIdToNavigationsMap.has(loaderId)) {
            return this.#loaderIdToNavigationsMap.get(loaderId);
        }
        if (this.#pendingNavigation !== undefined &&
            this.#pendingNavigation?.loaderId === undefined) {
            // This can be a pending navigation to `about:blank` created by a command. Use the
            // pending navigation in this case.
            return this.#pendingNavigation;
        }
        // Create a new pending navigation.
        return this.createPendingNavigation(url, true);
    }
    /**
     * @param {string} unreachableUrl indicated the navigation is actually failed.
     */
    frameNavigated(url, loaderId, unreachableUrl) {
        this.#logger?.(LogType.debug, `frameNavigated ${url}`);
        if (unreachableUrl !== undefined &&
            !this.#loaderIdToNavigationsMap.has(loaderId)) {
            // The navigation failed before started. Get or create pending navigation and fail
            // it.
            const navigation = this.#pendingNavigation ??
                this.createPendingNavigation(unreachableUrl, true);
            navigation.url = unreachableUrl;
            navigation.start();
            navigation.fail('the requested url is unreachable');
            return;
        }
        const navigation = this.#getNavigationForFrameNavigated(url, loaderId);
        navigation.frameNavigated();
        if (navigation !== this.#currentNavigation) {
            this.#currentNavigation.fail('navigation canceled by concurrent navigation');
        }
        navigation.url = url;
        navigation.loaderId = loaderId;
        this.#loaderIdToNavigationsMap.set(loaderId, navigation);
        navigation.start();
        this.#currentNavigation = navigation;
        if (this.#pendingNavigation === navigation) {
            this.#pendingNavigation = undefined;
        }
    }
    navigatedWithinDocument(url, navigationType) {
        this.#logger?.(LogType.debug, `navigatedWithinDocument ${url}, ${navigationType}`);
        // Current navigation URL should be updated.
        this.#currentNavigation.url = url;
        if (navigationType !== 'fragment') {
            // TODO: check for other navigation types, like `javascript`.
            return;
        }
        // There is no way to guaranteed match pending navigation with finished fragment
        // navigations. So assume any pending navigation without loader id is the fragment
        // one.
        const fragmentNavigation = this.#pendingNavigation !== undefined &&
            this.#pendingNavigation.loaderId === undefined
            ? this.#pendingNavigation
            : new NavigationState(url, this.#browsingContextId, false, this.#eventManager);
        // Finish ongoing navigation.
        fragmentNavigation.fragmentNavigated();
        if (fragmentNavigation === this.#pendingNavigation) {
            this.#pendingNavigation = undefined;
        }
    }
    frameRequestedNavigation(url) {
        this.#logger?.(LogType.debug, `Page.frameRequestedNavigation ${url}`);
        // The page is about to navigate to the url.
        this.createPendingNavigation(url, true);
    }
    /**
     * Required to mark navigation as fully complete.
     * TODO: navigation should be complete when it became the current one on
     * `Page.frameNavigated` or on navigating command finished with a new loader Id.
     */
    loadPageEvent(loaderId) {
        this.#logger?.(LogType.debug, 'loadPageEvent');
        // Even if it was an initial navigation, it is finished.
        this.#isInitialNavigation = false;
        this.#loaderIdToNavigationsMap.get(loaderId)?.load();
    }
    /**
     * Fail navigation due to navigation command failed.
     */
    failNavigation(navigation, errorText) {
        this.#logger?.(LogType.debug, 'failCommandNavigation');
        navigation.fail(errorText);
    }
    /**
     * Updates the navigation's `loaderId` and sets it as current one, if it is a
     * cross-document navigation.
     */
    navigationCommandFinished(navigation, loaderId) {
        this.#logger?.(LogType.debug, `finishCommandNavigation ${navigation.navigationId}, ${loaderId}`);
        if (loaderId !== undefined) {
            navigation.loaderId = loaderId;
            this.#loaderIdToNavigationsMap.set(loaderId, navigation);
        }
        if (loaderId === undefined || this.#currentNavigation === navigation) {
            // If the command's navigation is same-document or is already the current one,
            // nothing to do.
            return;
        }
        this.#currentNavigation.fail('navigation canceled by concurrent navigation');
        navigation.start();
        this.#currentNavigation = navigation;
        if (this.#pendingNavigation === navigation) {
            this.#pendingNavigation = undefined;
        }
    }
    /**
     * Emulated event, tight to `Network.requestWillBeSent`.
     */
    frameStartedNavigating(url, loaderId) {
        this.#logger?.(LogType.debug, `frameStartedNavigating ${url}, ${loaderId}`);
        if (this.#loaderIdToNavigationsMap.has(loaderId)) {
            // The `frameStartedNavigating` is tight to the `Network.requestWillBeSent` event
            // which can be emitted several times, e.g. in case of redirection. Nothing to do in
            // such a case.
            return;
        }
        const pendingNavigation = this.#pendingNavigation ?? this.createPendingNavigation(url, true);
        pendingNavigation.url = url;
        pendingNavigation.start();
        pendingNavigation.loaderId = loaderId;
        this.#loaderIdToNavigationsMap.set(loaderId, pendingNavigation);
    }
    /**
     * In case of `beforeunload` handler, the pending navigation should be marked as started
     * for consistency, as the `browsingContext.navigationStarted` should be emitted before
     * user prompt.
     */
    beforeunload() {
        this.#logger?.(LogType.debug, `beforeunload`);
        if (this.#pendingNavigation === undefined) {
            this.#logger?.(LogType.debugError, `Unexpectedly no pending navigation on beforeunload`);
            return;
        }
        this.#pendingNavigation.start();
    }
    /**
     * If there is a navigation with the loaderId equals to the network request id, it means
     * that the navigation failed.
     */
    networkLoadingFailed(loaderId, errorText) {
        this.#loaderIdToNavigationsMap.get(loaderId)?.fail(errorText);
    }
}
//# sourceMappingURL=NavigationTracker.js.map