// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Chrome-WebUI-side of the Glic API.
// Communicates with the web client side in ../client/.
import { assert } from '//resources/js/assert.js';
import { loadTimeData } from '//resources/js/load_time_data.js';
import { ActorClientReceiver, ActorHandlerRemote, AnnotationHandlerRemote, WebClientHandlerRemote } from '../../glic.mojom-webui.js';
import { ObservableValue } from '../../observable.js';
import { TaskQueue } from '../../task_queue.js';
import { OneShotTimer } from '../../timer.js';
import { ActorClientImpl, ActorHostMessageHandler } from '../actor/actor_host.js';
import { ActorClientDef, ActorHostDef } from '../actor/actor_types.js';
import { AnnotationHostMessageHandler } from '../annotation/annotation_host.js';
import { AnnotationHostDef } from '../annotation/annotation_types.js';
import { createBidirectionalPostMessageTransport } from '../transport/post_message_transport.js';
import { ERROR_CODEC, getHostRequestHistogramInfo, MAX_REQUEST_ID, WebClientDef, WebClientHostDef } from './../request_types.js';
import { urlFromClient } from './conversions.js';
import { HostMessageHandler } from './host_from_client.js';
import { PanelOpenState } from './types.js';
export var WebClientState;
(function (WebClientState) {
    WebClientState[WebClientState["UNINITIALIZED"] = 0] = "UNINITIALIZED";
    WebClientState[WebClientState["RESPONSIVE"] = 1] = "RESPONSIVE";
    WebClientState[WebClientState["UNRESPONSIVE"] = 2] = "UNRESPONSIVE";
    WebClientState[WebClientState["ERROR"] = 3] = "ERROR";
})(WebClientState || (WebClientState = {}));
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
//
// LINT.IfChange(DetailedWebClientState)
export var DetailedWebClientState;
(function (DetailedWebClientState) {
    DetailedWebClientState[DetailedWebClientState["BOOTSTRAP_PENDING"] = 0] = "BOOTSTRAP_PENDING";
    DetailedWebClientState[DetailedWebClientState["WEB_CLIENT_NOT_CREATED"] = 1] = "WEB_CLIENT_NOT_CREATED";
    DetailedWebClientState[DetailedWebClientState["WEB_CLIENT_INITIALIZE_FAILED"] = 2] = "WEB_CLIENT_INITIALIZE_FAILED";
    DetailedWebClientState[DetailedWebClientState["WEB_CLIENT_NOT_INITIALIZED"] = 3] = "WEB_CLIENT_NOT_INITIALIZED";
    DetailedWebClientState[DetailedWebClientState["TEMPORARY_UNRESPONSIVE"] = 4] = "TEMPORARY_UNRESPONSIVE";
    DetailedWebClientState[DetailedWebClientState["PERMANENT_UNRESPONSIVE"] = 5] = "PERMANENT_UNRESPONSIVE";
    DetailedWebClientState[DetailedWebClientState["RESPONSIVE"] = 6] = "RESPONSIVE";
    DetailedWebClientState[DetailedWebClientState["RESPONSIVE_INACTIVE"] = 7] = "RESPONSIVE_INACTIVE";
    DetailedWebClientState[DetailedWebClientState["UNRESPONSIVE_INACTIVE"] = 8] = "UNRESPONSIVE_INACTIVE";
    // OBSOLETE: MOJO_PIPE_CLOSED_UNEXPECTEDLY = 9,
    DetailedWebClientState[DetailedWebClientState["MOJO_PIPE_CLOSED_UNEXPECTEDLY_BEFORE_INITIALIZE"] = 10] = "MOJO_PIPE_CLOSED_UNEXPECTEDLY_BEFORE_INITIALIZE";
    DetailedWebClientState[DetailedWebClientState["MOJO_PIPE_CLOSED_UNEXPECTEDLY_AFTER_INITIALIZE"] = 11] = "MOJO_PIPE_CLOSED_UNEXPECTEDLY_AFTER_INITIALIZE";
    DetailedWebClientState[DetailedWebClientState["MAX_VALUE"] = 11] = "MAX_VALUE";
})(DetailedWebClientState || (DetailedWebClientState = {}));
// Sets up communication with the client.
// This is separate from GlicApiHost to allow us to detect the client page
// before our host is really ready to connect.
export class GlicApiCommunicator {
    embeddedOrigin;
    windowProxy;
    postMessageReceiver;
    postMessageSender;
    pmRemote;
    router;
    bootstrapPingIntervalId;
    loggingEnabled = loadTimeData.getBoolean('loggingEnabled');
    host;
    hostPromise = Promise.withResolvers();
    rootReceiver;
    constructor(embeddedOrigin, windowProxy) {
        this.embeddedOrigin = embeddedOrigin;
        this.windowProxy = windowProxy;
        const { router, sender, receiver, rootRemote, rootReceiver } = createBidirectionalPostMessageTransport(embeddedOrigin, windowProxy, /*lifecycleObserver=*/ {}, this, 'glic_api_host', true, ERROR_CODEC, WebClientHostDef, WebClientDef);
        this.rootReceiver = rootReceiver;
        this.pmRemote = rootRemote;
        this.router = router;
        this.postMessageReceiver = receiver;
        this.postMessageSender = sender;
        this.router.setLoggingEnabled(this.loggingEnabled);
        this.postMessageSender.setMaxInFlightRequests(loadTimeData.getInteger('maxInFlightRequests'));
        this.postMessageSender.sendResponsesForAllRequests =
            loadTimeData.getBoolean('sendResponsesForAllRequests');
        this.bootstrapPingIntervalId =
            window.setInterval(this.bootstrapPing.bind(this), 50);
        this.bootstrapPing();
    }
    destroy() {
        window.clearInterval(this.bootstrapPingIntervalId);
        this.router.destroy();
    }
    // Should be called only once.
    setHost(host) {
        assert(!this.host);
        this.host = host;
        this.hostPromise.resolve(host);
    }
    // Called when the webview page is loaded.
    contentLoaded() {
        // Send the ping message one more time. At this point, the webview should
        // be able to handle the message, if it hasn't already.
        this.bootstrapPing();
        this.stopBootstrapPing();
    }
    // Intercept initial handshake on pipe 0.
    async webClientCreated(payload, extras) {
        this.stopBootstrapPing();
        const h = await this.hostPromise.promise;
        this.postMessageReceiver.requestObserver = h;
        this.postMessageReceiver.setHandlerWrapper(h.handlerWrapper.bind(h));
        this.rootReceiver.setMessageHandler(h.hostMessageHandler, WebClientHostDef);
        const handleFn = h.hostMessageHandler['webClientCreated'];
        if (!handleFn) {
            return undefined;
        }
        return await handleFn.call(h.hostMessageHandler, payload, extras);
    }
    stopBootstrapPing() {
        if (this.bootstrapPingIntervalId !== undefined) {
            window.clearInterval(this.bootstrapPingIntervalId);
            this.bootstrapPingIntervalId = undefined;
        }
    }
    // Sends a message to the webview which is required to initialize the client.
    // Because we don't know when the client will be ready to receive this
    // message, we start sending this every 50ms as soon as navigation commits on
    // the webview, and stop sending this when the page loads, or we receive a
    // request from the client.
    bootstrapPing() {
        if (this.bootstrapPingIntervalId === undefined) {
            return;
        }
        this.windowProxy.postMessage({
            type: 'glic-bootstrap',
            glicApiSource: loadTimeData.getString('glicGuestAPISource'),
        }, this.embeddedOrigin);
    }
}
/**
 * The host side of the Glic API.
 *
 * Its primary job is to route calls between the client (over postMessage) and
 * the browser (over Mojo).
 */
export class GlicApiHost {
    browserProxy;
    communicator;
    embedder;
    hostMessageHandler;
    sender;
    panelIsActive = false;
    handler;
    webClientErrorTimer;
    webClientState = ObservableValue.withValue(WebClientState.UNINITIALIZED);
    openCloseTasks = new TaskQueue();
    waitingOnPanelWillOpenValue = false;
    // Synchronizes panel open/close events between the browser and client,
    // ensuring panel open state is eventually consistent.
    clientActiveObs = ObservableValue.withValue(false);
    // The open state as understood by the client, this is delayed
    // from the notifyPanelWillOpen and notifyPanelWasClosed calls because
    // processing is async.
    panelOpenState = PanelOpenState.CLOSED;
    instanceIsActive = true;
    hasShownDebuggerAttachedWarning = false;
    detailedWebClientState = DetailedWebClientState.BOOTSTRAP_PENDING;
    // Present while the client is monitoring pin candidates.
    pinCandidatesObserver;
    captureRegionObserver;
    actorHandler;
    annotationHandler;
    isSubscribedToZoomLevel = false;
    experimentalTriggeringUpdatesHandler = new Map();
    nextExperimentalTriggeringUpdateHandlerId = 0;
    constructor(browserProxy, communicator, embedder) {
        this.browserProxy = browserProxy;
        this.communicator = communicator;
        this.embedder = embedder;
        this.sender = communicator.pmRemote;
        this.handler = new WebClientHandlerRemote();
        this.handler.onConnectionError.addListener(() => {
            if (this.webClientState.getCurrentValue() !== WebClientState.ERROR) {
                console.warn(`Mojo connection error in glic host`);
                this.detailedWebClientState = this.detailedWebClientState ===
                    DetailedWebClientState.BOOTSTRAP_PENDING ?
                    DetailedWebClientState
                        .MOJO_PIPE_CLOSED_UNEXPECTEDLY_BEFORE_INITIALIZE :
                    DetailedWebClientState
                        .MOJO_PIPE_CLOSED_UNEXPECTEDLY_AFTER_INITIALIZE;
                this.webClientState.assignAndSignal(WebClientState.ERROR);
            }
        });
        this.handler.$.close();
        this.browserProxy.pageHandler.createWebClient(this.handler.$.bindNewPipeAndPassReceiver());
        this.hostMessageHandler =
            new HostMessageHandler(this.handler, embedder, this);
        this.webClientErrorTimer = new OneShotTimer(loadTimeData.getInteger('clientUnresponsiveUiMaxTimeMs'));
        communicator.setHost(this);
    }
    destroy() {
        this.webClientState = ObservableValue.withValue(WebClientState.ERROR); // Final state
        this.webClientErrorTimer.reset();
        this.hostMessageHandler.destroy();
        this.pinCandidatesObserver?.disconnectFromSource();
        this.captureRegionObserver?.destroy();
        if (this.actorHandler) {
            this.actorHandler.$.close();
            this.actorHandler = undefined;
        }
        if (this.annotationHandler) {
            this.annotationHandler.$.close();
            this.annotationHandler = undefined;
        }
        for (const handler of this.experimentalTriggeringUpdatesHandler.values()) {
            handler.$.close();
        }
        this.experimentalTriggeringUpdatesHandler.clear();
    }
    setInitialState(initialState) {
        this.panelIsActive = initialState.panelIsActive;
        if (!initialState.enableActInFocusedTab) {
            return {};
        }
        this.actorHandler = new ActorHandlerRemote();
        const { remote: clientRemote, receiver: actorReceiver } = this.communicator.router.newPipeWithRemote(ActorClientDef);
        const actorClientReceiver = new ActorClientReceiver(new ActorClientImpl(clientRemote));
        this.handler.createActorHandler(this.actorHandler.$.bindNewPipeAndPassReceiver(), actorClientReceiver.$.bindNewPipeAndPassRemote());
        const actorHostMessageHandler = new ActorHostMessageHandler(this.actorHandler);
        const { remote: actorRemote /* receiver never closed */ } = this.communicator.router.newPipeWithReceiver(actorHostMessageHandler, ActorHostDef);
        return {
            actorRemote,
            actorReceiver,
        };
    }
    createAnnotationHandler(receiver) {
        assert(!this.annotationHandler);
        this.annotationHandler = new AnnotationHandlerRemote();
        this.handler.createAnnotationHandler(this.annotationHandler.$.bindNewPipeAndPassReceiver());
        const annotationHostMessageHandler = new AnnotationHostMessageHandler(this.annotationHandler);
        this.communicator.router.newReceiver(receiver, annotationHostMessageHandler, AnnotationHostDef);
    }
    async subscribeToZoomLevel() {
        this.isSubscribedToZoomLevel = true;
        try {
            const zoomFactor = await this.embedder.getZoom();
            this.sender.requestNoResponse('notifyZoomLevelChanged', { zoomFactor });
        }
        catch (e) {
            console.warn('Failed to get initial zoom level', e);
        }
    }
    unsubscribeFromZoomLevel() {
        this.isSubscribedToZoomLevel = false;
    }
    onZoomLevelChanged(zoomFactor) {
        if (this.isSubscribedToZoomLevel) {
            this.sender.requestNoResponse('notifyZoomLevelChanged', { zoomFactor });
        }
    }
    waitingOnPanelWillOpen() {
        return this.waitingOnPanelWillOpenValue;
    }
    setWaitingOnPanelWillOpen(value) {
        this.waitingOnPanelWillOpenValue = value;
    }
    panelOpenStateChanged(state) {
        this.panelOpenState = state;
        this.clientActiveObs.assignAndSignal(this.isClientActive());
        if (state === PanelOpenState.CLOSED) {
            this.pinCandidatesObserver?.disconnectFromSource();
        }
        else {
            this.pinCandidatesObserver?.connectToSource();
        }
    }
    setInstanceIsActive(instanceIsActive) {
        this.instanceIsActive = instanceIsActive;
        this.clientActiveObs.assignAndSignal(this.isClientActive());
    }
    // Returns true if the user might be interacting with the client.
    // That is, the panel is open, not in an error state, and either the panel
    // itself is focused or a browser window it could be accessing is.
    isClientActive() {
        return this.panelOpenState === PanelOpenState.OPEN &&
            this.webClientState.getCurrentValue() !== WebClientState.ERROR &&
            this.instanceIsActive;
    }
    // Called when the web client is initialized.
    webClientInitialized() {
        this.detailedWebClientState = DetailedWebClientState.RESPONSIVE;
        this.setWebClientState(WebClientState.RESPONSIVE);
        this.responsiveCheckLoop();
    }
    webClientInitializeFailed() {
        console.warn('GlicApiHost: web client initialize failed');
        this.detailedWebClientState =
            DetailedWebClientState.WEB_CLIENT_INITIALIZE_FAILED;
        this.setWebClientState(WebClientState.ERROR);
    }
    setWebClientState(state) {
        this.webClientState.assignAndSignal(state);
    }
    getWebClientState() {
        return this.webClientState;
    }
    getDetailedWebClientState() {
        return this.detailedWebClientState;
    }
    async responsiveCheckLoop() {
        if (!loadTimeData.getBoolean('isClientResponsivenessCheckEnabled')) {
            return;
        }
        // Timeout duration for waiting for a response. Increased in dev mode.
        const timeoutMs = loadTimeData.getInteger('clientResponsivenessCheckTimeoutMs') *
            (loadTimeData.getBoolean('devMode') ? 1000 : 1);
        // Interval in between the consecutive checks.
        const checkIntervalMs = loadTimeData.getInteger('clientResponsivenessCheckIntervalMs');
        while (this.webClientState.getCurrentValue() !== WebClientState.ERROR) {
            if (!this.isClientActive()) {
                if (this.webClientState.getCurrentValue() ===
                    WebClientState.UNRESPONSIVE) {
                    this.detailedWebClientState =
                        DetailedWebClientState.UNRESPONSIVE_INACTIVE;
                    // Prevent unresponsive overlay showing forever while checking is
                    // paused.
                    this.setWebClientState(WebClientState.RESPONSIVE);
                    this.webClientErrorTimer.reset();
                }
                else {
                    this.detailedWebClientState =
                        DetailedWebClientState.RESPONSIVE_INACTIVE;
                }
                await this.clientActiveObs.waitUntil((active) => active);
            }
            const SMALL_QUEUE_SIZE = 50;
            const hostSendMessageQueueLength = this.sender.rawSender().messageQueueLength() +
                this.sender.rawSender().inFlightRequestCount();
            if (hostSendMessageQueueLength >= SMALL_QUEUE_SIZE) {
                chrome.histograms.recordMediumCount('Glic.Host.HostSendMessageQueueLength', hostSendMessageQueueLength);
            }
            let gotResponse = false;
            const responsePromise = this.sender.requestWithResponse('checkResponsive', undefined)
                .then((response) => {
                gotResponse = true;
                if (response.clientSendMessageQueueLength >= SMALL_QUEUE_SIZE) {
                    chrome.histograms.recordMediumCount('Glic.Host.ClientSendMessageQueueLength', response.clientSendMessageQueueLength);
                }
            });
            const responseTimeout = sleep(timeoutMs);
            await Promise.race([responsePromise, responseTimeout]);
            if (this.webClientState.getCurrentValue() === WebClientState.ERROR) {
                return; // ERROR state is final.
            }
            if (gotResponse) { // Success
                this.webClientErrorTimer.reset();
                this.setWebClientState(WebClientState.RESPONSIVE);
                this.detailedWebClientState = DetailedWebClientState.RESPONSIVE;
                await sleep(checkIntervalMs);
                continue;
            }
            // Failed, not responsive.
            if (this.webClientState.getCurrentValue() === WebClientState.RESPONSIVE) {
                const ignoreUnresponsiveClient = await this.shouldAllowUnresponsiveClient();
                if (!ignoreUnresponsiveClient) {
                    console.warn('GlicApiHost: web client is unresponsive');
                    this.detailedWebClientState =
                        DetailedWebClientState.TEMPORARY_UNRESPONSIVE;
                    this.setWebClientState(WebClientState.UNRESPONSIVE);
                    this.startWebClientErrorTimer();
                }
            }
            // Crucial: Wait for the original (late) response promise to settle before
            // the next check cycle starts.
            await responsePromise;
        }
    }
    async shouldAllowUnresponsiveClient() {
        if (loadTimeData.getBoolean('clientResponsivenessCheckIgnoreWhenDebuggerAttached')) {
            const isDebuggerAttached = await this.handler.isDebuggerAttached()
                .then(result => result.isAttachedToWebview)
                .catch(() => false);
            if (isDebuggerAttached) {
                if (!this.hasShownDebuggerAttachedWarning) {
                    console.warn('GlicApiHost: ignoring unresponsive client because ' +
                        'a debugger (likely DevTools) is attached');
                    this.hasShownDebuggerAttachedWarning = true;
                }
                return true;
            }
        }
        return false;
    }
    startWebClientErrorTimer() {
        this.webClientErrorTimer.start(() => {
            console.warn('GlicApiHost: web client is permanently unresponsive');
            this.detailedWebClientState =
                DetailedWebClientState.PERMANENT_UNRESPONSIVE;
            this.setWebClientState(WebClientState.ERROR);
        });
    }
    openLinkInPopup(url, initialWidth, initialHeight) {
        this.handler.openLinkInPopup(urlFromClient(url), initialWidth, initialHeight);
    }
    async openLinkInNewTab(url) {
        await this.handler.createTab(urlFromClient(url), false, null);
    }
    async shouldAllowMediaPermissionRequest() {
        return (await this.handler.shouldAllowMediaPermissionRequest()).isAllowed;
    }
    async shouldAllowGeolocationPermissionRequest() {
        return (await this.handler.shouldAllowGeolocationPermissionRequest())
            .isAllowed;
    }
    async handlerWrapper(type, interfaceDef, payload, extras, handlerFunction) {
        if (this.detailedWebClientState ===
            DetailedWebClientState.BOOTSTRAP_PENDING) {
            this.detailedWebClientState =
                DetailedWebClientState.WEB_CLIENT_NOT_CREATED;
        }
        const startTime = performance.now();
        const response = await handlerFunction(payload, extras);
        if (response) {
            // Report latency metric for handled requests that return a response.
            const latency = performance.now() - startTime;
            this.reportLatency(type, interfaceDef, latency);
        }
        // Not all request types require a return value.
        return response;
    }
    onRequestReceived(type, interfaceDef) {
        this.reportRequestCountEvent(type, interfaceDef, GlicRequestEvent.REQUEST_RECEIVED);
        if (!this.panelIsActive) {
            this.reportRequestCountEvent(type, interfaceDef, GlicRequestEvent.REQUEST_RECEIVED_WHILE_INACTIVE);
        }
    }
    onRequestHandlerException(type, interfaceDef) {
        this.reportRequestCountEvent(type, interfaceDef, GlicRequestEvent.REQUEST_HANDLER_EXCEPTION);
    }
    onRequestCompleted(type, interfaceDef) {
        this.reportRequestCountEvent(type, interfaceDef, GlicRequestEvent.RESPONSE_SENT);
    }
    reportRequestCountEvent(requestType, interfaceDef, event) {
        const histogramInfo = getHostRequestHistogramInfo(requestType, interfaceDef);
        if (histogramInfo === undefined) {
            return;
        }
        chrome.histograms.recordEnumerationValue(`Glic.Api.RequestCounts.${histogramInfo.name}`, event, GlicRequestEvent.MAX_VALUE + 1);
        switch (event) {
            case GlicRequestEvent.REQUEST_HANDLER_EXCEPTION:
                chrome.histograms.recordEnumerationValue(`Glic.Api.StatusCounts.Error`, histogramInfo.id, MAX_REQUEST_ID + 1);
                break;
            case GlicRequestEvent.REQUEST_RECEIVED_WHILE_INACTIVE:
                chrome.histograms.recordEnumerationValue(`Glic.Api.StatusCounts.Inactive`, histogramInfo.id, MAX_REQUEST_ID + 1);
                break;
            case GlicRequestEvent.REQUEST_RECEIVED:
                chrome.histograms.recordEnumerationValue(`Glic.Api.StatusCounts.Received`, histogramInfo.id, MAX_REQUEST_ID + 1);
                break;
            default:
                break;
        }
    }
    addExperimentalTriggeringUpdatesHandler(handler) {
        const id = this.nextExperimentalTriggeringUpdateHandlerId++;
        this.experimentalTriggeringUpdatesHandler.set(id, handler);
        return id;
    }
    getExperimentalTriggeringUpdatesHandler(observationId) {
        return this.experimentalTriggeringUpdatesHandler.get(observationId);
    }
    deleteExperimentalTriggeringUpdatesHandler(observationId) {
        this.experimentalTriggeringUpdatesHandler.delete(observationId);
    }
    reportLatency(requestType, interfaceDef, latencyMs) {
        const histogramInfo = getHostRequestHistogramInfo(requestType, interfaceDef);
        if (histogramInfo === undefined) {
            return;
        }
        chrome.histograms.recordTime(`Glic.Api.RequestHostLatency.${histogramInfo.name}`, Math.round(latencyMs));
    }
}
// LINT.IfChange(GlicRequestEvent)
var GlicRequestEvent;
(function (GlicRequestEvent) {
    GlicRequestEvent[GlicRequestEvent["REQUEST_RECEIVED"] = 0] = "REQUEST_RECEIVED";
    GlicRequestEvent[GlicRequestEvent["RESPONSE_SENT"] = 1] = "RESPONSE_SENT";
    GlicRequestEvent[GlicRequestEvent["REQUEST_HANDLER_EXCEPTION"] = 2] = "REQUEST_HANDLER_EXCEPTION";
    // Deprecated: REQUEST_RECEIVED_WHILE_HIDDEN = 3,
    GlicRequestEvent[GlicRequestEvent["REQUEST_RECEIVED_WHILE_INACTIVE"] = 4] = "REQUEST_RECEIVED_WHILE_INACTIVE";
    GlicRequestEvent[GlicRequestEvent["MAX_VALUE"] = 4] = "MAX_VALUE";
})(GlicRequestEvent || (GlicRequestEvent = {}));
// LINT.ThenChange(//tools/metrics/histograms/metadata/glic/enums.xml:GlicRequestEvent)
// Returns a Promise resolving after 'ms' milliseconds
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}
