/**
 * 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 path from 'path';
import os from 'os';
import fs from 'fs';
import crypto from 'crypto';
import { execFile } from 'child_process';
import { eventsHelper } from '@utils/eventsHelper';
import { createClientInfo } from '../cli-client/registry';
import { SessionProviderEvent } from './sessionProvider';
export class DashboardConnection {
    sendEvent;
    close;
    _provider;
    _attachedPage;
    _onclose;
    _onconnected;
    _onAnnotationSubmit;
    _pushTabsScheduled = false;
    _visible = true;
    _pendingReveal;
    _annotateWaitingForAttach = false;
    _recordingDir;
    _streams = new Map();
    constructor(provider, onclose, onconnected, onAnnotationSubmit) {
        this._provider = provider;
        this._onclose = onclose;
        this._onconnected = onconnected;
        this._onAnnotationSubmit = onAnnotationSubmit;
        this._recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'playwright-recordings-'));
    }
    onconnect() {
        this._provider.on(SessionProviderEvent.SessionsChanged, () => {
            this._pushSessions();
            void this._tryRevealPending();
        });
        this._provider.on(SessionProviderEvent.TabsChanged, () => this._pushTabs());
        this._provider.on(SessionProviderEvent.ContextClosed, context => {
            if (this._attachedPage?.page.context() === context) {
                this._attachedPage.dispose();
                this._attachedPage = undefined;
            }
        });
        this._provider.on(SessionProviderEvent.AttachRequested, page => { void this._switchAttachedTo(page); });
        this._provider.start();
        this._onconnected?.();
    }
    onclose() {
        this._provider.dispose();
        this._attachedPage?.dispose();
        this._attachedPage = undefined;
        for (const stream of this._streams.values()) {
            void stream.handle.close()
                .catch(() => { })
                .then(() => fs.promises.unlink(stream.path))
                .catch(() => { });
        }
        this._streams.clear();
        this._onclose();
    }
    async dispatch(method, params) {
        // eslint-disable-next-line no-restricted-syntax
        const handler = this[method];
        if (typeof handler === 'function')
            return handler.call(this, params);
        const attached = this._attachedPage;
        if (!attached)
            return;
        // eslint-disable-next-line no-restricted-syntax
        const onAtt = attached[method];
        if (typeof onAtt === 'function')
            return onAtt.call(attached, params);
    }
    async selectTab(params) {
        const page = this._provider.findPage(params);
        if (page)
            await this._switchAttachedTo(page);
        this._pushTabs();
    }
    async newTab(params) {
        const context = this._provider.findContext(params);
        if (!context)
            return;
        const page = await context.newPage();
        await this._switchAttachedTo(page);
        this._pushTabs();
    }
    async closeTab(params) {
        const page = this._provider.findPage(params);
        await page?.close({ reason: 'Closed in Dashboard' });
    }
    async closeSession(params) {
        await this._provider.closeSession(params.browser);
    }
    async setVisible(params) {
        if (this._visible === params.visible)
            return;
        this._visible = params.visible;
        await this._attachedPage?.setScreencastActive(params.visible);
    }
    revealSession(sessionName, workspaceDir) {
        this._pendingReveal = { sessionName, workspaceDir };
        void this._tryRevealPending();
    }
    revealPage(pageId) {
        this._pendingReveal = { pageId };
        void this._tryRevealPending();
    }
    async _tryRevealPending() {
        const pending = this._pendingReveal;
        if (!pending)
            return;
        const allPages = this._provider.contextEntries().flatMap(e => e.context.pages().map(page => ({ entry: e, page })));
        let page;
        if (pending.pageId !== undefined) {
            page = allPages.find(({ page: p }) => pageId(p) === pending.pageId)?.page;
        }
        else if (pending.sessionName !== undefined) {
            page = allPages.find(({ entry }) => entry.descriptor.title === pending.sessionName
                && (pending.workspaceDir === undefined || entry.descriptor.workspaceDir === pending.workspaceDir))?.page;
        }
        if (!page)
            return;
        this._pendingReveal = undefined;
        await this._switchAttachedTo(page);
        this._pushTabs();
    }
    async submitAnnotation(params) {
        this._onAnnotationSubmit?.(params.data, params.ariaSnapshot, params.annotations);
    }
    async reveal(params) {
        switch (os.platform()) {
            case 'darwin':
                execFile('open', ['-R', params.path]);
                break;
            case 'win32':
                execFile('explorer', ['/select,', params.path]);
                break;
            case 'linux':
                execFile('xdg-open', [path.dirname(params.path)]);
                break;
        }
    }
    async readStream(params) {
        const stream = this._streams.get(params.streamId);
        if (!stream)
            throw new Error(`Unknown stream: ${params.streamId}`);
        const buffer = Buffer.alloc(256 * 1024);
        const { bytesRead } = await stream.handle.read(buffer, 0, buffer.length);
        if (bytesRead === 0) {
            this._streams.delete(params.streamId);
            await stream.handle.close().catch(() => { });
            await fs.promises.unlink(stream.path).catch(() => { });
            return { data: '', eof: true };
        }
        return { data: buffer.subarray(0, bytesRead).toString('base64'), eof: false };
    }
    visible() {
        return this._visible;
    }
    emitSessions(sessions) {
        this.sendEvent?.('sessions', { sessions, clientInfo: createClientInfo() });
    }
    emitTabs(tabs) {
        this.sendEvent?.('tabs', { tabs });
    }
    emitFrame(data, viewportWidth, viewportHeight) {
        this.sendEvent?.('frame', { data, viewportWidth, viewportHeight });
    }
    emitAnnotate() {
        // Defer until a page is attached so the client can fetch a screenshot.
        if (!this._attachedPage) {
            this._annotateWaitingForAttach = true;
            return;
        }
        this.sendEvent?.('annotate', {});
    }
    emitCancelAnnotate() {
        this._annotateWaitingForAttach = false;
        this.sendEvent?.('cancelAnnotate', {});
    }
    artifactsDirFor(context) {
        for (const entry of this._provider.contextEntries()) {
            if (entry.context === context)
                return entry.descriptor.browser.launchOptions.artifactsDir ?? this._recordingDir;
        }
        return this._recordingDir;
    }
    _pushTabs() {
        if (this._pushTabsScheduled)
            return;
        this._pushTabsScheduled = true;
        queueMicrotask(async () => {
            this._pushTabsScheduled = false;
            try {
                const tabs = await this._aggregateTabs();
                this.emitTabs(tabs);
            }
            catch {
                // best-effort
            }
        });
    }
    _pushSessions() {
        void (async () => {
            try {
                const sessions = await this._provider.sessions();
                this.emitSessions(sessions);
            }
            catch {
                // best-effort
            }
        })();
    }
    async _aggregateTabs() {
        const attachedPage = this._attachedPage?.page;
        const tasks = [];
        for (const { browser, context } of this._provider.contextEntries()) {
            for (const page of context.pages()) {
                tasks.push((async () => ({
                    browser: browserId(browser),
                    context: contextId(context),
                    page: pageId(page),
                    title: await page.title().catch(() => ''),
                    url: page.url(),
                    selected: page === attachedPage,
                    faviconUrl: await faviconUrl(page),
                }))());
            }
        }
        return await Promise.all(tasks);
    }
    async _switchAttachedTo(page) {
        if (this._attachedPage?.page === page)
            return;
        this._attachedPage?.dispose();
        const attached = new AttachedPage(this, page);
        this._attachedPage = attached;
        try {
            await attached.init();
        }
        catch (e) {
            if (this._attachedPage === attached)
                this._attachedPage = undefined;
            attached.dispose();
            throw e;
        }
        if (this._annotateWaitingForAttach && this._attachedPage === attached) {
            this._annotateWaitingForAttach = false;
            this.sendEvent?.('annotate', {});
        }
    }
    _handleAttachedPageClose(context) {
        this._attachedPage?.dispose();
        this._attachedPage = undefined;
        const next = context.pages()[0];
        if (next)
            void this._switchAttachedTo(next);
        this._pushTabs();
    }
}
class AttachedPage {
    _owner;
    _page;
    _listeners = [];
    _screencastRunning = false;
    _recordingPath = null;
    _disposed = false;
    constructor(owner, page) {
        this._owner = owner;
        this._page = page;
    }
    get page() { return this._page; }
    async init() {
        this._listeners.push(eventsHelper.addEventListener(this._page, 'close', () => {
            this._owner._handleAttachedPageClose(this._page.context());
        }), eventsHelper.addEventListener(this._page, 'framenavigated', (frame) => {
            if (frame === this._page.mainFrame())
                this._owner._pushTabs();
        }));
        this._owner._pushTabs();
        if (this._owner.visible()) {
            this._screencastRunning = true;
            await this._startScreencast(this._page);
        }
    }
    dispose() {
        this._disposed = true;
        this._listeners.forEach(d => d.dispose());
        this._listeners = [];
        if (this._screencastRunning)
            this._page.screencast.stop().catch(() => { });
        this._screencastRunning = false;
        this._recordingPath = null;
    }
    async setScreencastActive(active) {
        if (active && !this._screencastRunning) {
            this._screencastRunning = true;
            await this._startScreencast(this._page);
        }
        else if (!active && this._screencastRunning) {
            this._screencastRunning = false;
            await this._page.screencast.stop().catch(() => { });
        }
    }
    async navigate(params) {
        if (!params.url)
            return;
        await this._page.goto(params.url);
    }
    async back() {
        await this._page.goBack();
    }
    async forward() {
        await this._page.goForward();
    }
    async reload() {
        await this._page.reload();
    }
    async mousemove(params) {
        await this._page.mouse.move(params.x, params.y);
    }
    async mousedown(params) {
        await this._page.mouse.move(params.x, params.y);
        await this._page.mouse.down({ button: params.button || 'left' });
    }
    async mouseup(params) {
        await this._page.mouse.move(params.x, params.y);
        await this._page.mouse.up({ button: params.button || 'left' });
    }
    async wheel(params) {
        await this._page.mouse.wheel(params.deltaX, params.deltaY);
    }
    async keydown(params) {
        await this._page.keyboard.down(params.key);
    }
    async keyup(params) {
        await this._page.keyboard.up(params.key);
    }
    async startRecording() {
        const artifactsDir = this._owner.artifactsDirFor(this._page.context());
        this._recordingPath = path.join(artifactsDir, `recording-${Date.now()}.webm`);
        if (this._screencastRunning)
            await this._restartScreencast(this._page);
    }
    async stopRecording() {
        const p = this._recordingPath;
        if (!p)
            throw new Error('No recording in progress');
        this._recordingPath = null;
        if (this._screencastRunning)
            await this._restartScreencast(this._page);
        const handle = await fs.promises.open(p, 'r');
        const streamId = crypto.randomUUID();
        this._owner._streams.set(streamId, { handle, path: p });
        return { streamId };
    }
    async screenshot() {
        const buffer = await this._page.screenshot({ type: 'png' });
        const vp = await this._viewportSize();
        const ariaSnapshot = await this._page.ariaSnapshot({ boxes: true, mode: 'ai' });
        return {
            data: buffer.toString('base64'),
            viewportWidth: vp.width,
            viewportHeight: vp.height,
            ariaSnapshot,
        };
    }
    async _viewportSize() {
        // Pages whose context was created with `viewport: null` (e.g. headed `playwright-cli open --headed`)
        // have no fixed viewport, so `viewportSize()` returns null. Fall back to the live window size.
        const vp = this._page.viewportSize();
        if (vp)
            return vp;
        return await this._page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight }));
    }
    async _startScreencast(page) {
        await page.screencast.start({
            onFrame: ({ data }) => {
                if (this._disposed)
                    return;
                const vp = page.viewportSize();
                this._owner.emitFrame(data.toString('base64'), vp?.width ?? 0, vp?.height ?? 0);
            },
            size: { width: 1280, height: 800 },
            ...(this._recordingPath ? { path: this._recordingPath } : {}),
        });
    }
    async _restartScreencast(page) {
        await page.screencast.stop().catch(() => { });
        await this._startScreencast(page);
    }
}
function browserId(browser) {
    // eslint-disable-next-line no-restricted-syntax -- _guid is very conservative.
    return browser._guid;
}
function pageId(p) {
    // eslint-disable-next-line no-restricted-syntax -- _guid is very conservative.
    return p._guid;
}
function contextId(c) {
    // eslint-disable-next-line no-restricted-syntax -- _guid is very conservative.
    return c._guid;
}
async function faviconUrl(page) {
    const url = page.evaluate(async () => {
        const response = await fetch(document.querySelector('link[rel~="icon"]')?.href ?? '/favicon.ico');
        if (!response.ok)
            return undefined;
        const blob = await response.blob();
        if (!blob.type.startsWith('image/'))
            return undefined;
        return await new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onloadend = () => resolve(reader.result);
            reader.onerror = reject;
            reader.readAsDataURL(blob);
        });
    }).catch(() => undefined);
    const timeout = new Promise(resolve => setTimeout(() => resolve(undefined), 3000));
    return await Promise.race([url, timeout]);
}
//# sourceMappingURL=dashboardController.js.map