/**
 * 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 React from 'react';
import './dashboard.css';
import { ChevronLeftIcon, ChevronRightIcon, LockIcon, LockOpenIcon, ReloadIcon, ScreenshotRegionIcon } from './icons';
import { AnnotateModal } from './annotations';
import { clientToViewport, getImageLayout } from './imageLayout';
import { Recording } from './recording';
import { ToolbarButton } from '@web/components/toolbarButton';
import { useMeasureForRef } from '@web/uiUtils';
const BUTTONS = ['left', 'middle', 'right'];
async function pickSaveWritable(suggestedName, description, mime, extension) {
    try {
        const handle = await window.showSaveFilePicker({
            suggestedName,
            types: [{ description, accept: { [mime]: [extension] } }],
        });
        return await handle.createWritable();
    }
    catch {
        return null;
    }
}
function smartUrl(input) {
    const value = input.trim();
    if (!value)
        return value;
    if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value) || value.startsWith('about:') || value.startsWith('data:'))
        return value;
    const host = value.split(/[/?#]/, 1)[0];
    const hasDot = host.includes('.');
    const isLocalhost = /^localhost(:\d+)?$/i.test(host);
    const hasPort = /:\d+$/.test(host);
    const isIp = /^\d{1,3}(\.\d{1,3}){3}(:\d+)?$/.test(host);
    if (isLocalhost || isIp || (hasPort && !hasDot))
        return 'http://' + value;
    if (hasDot || hasPort)
        return 'https://' + value;
    return 'https://' + host + '.com' + value.slice(host.length);
}
export const Dashboard = ({ model }) => {
    const [, setRevision] = React.useState(0);
    React.useEffect(() => model.subscribe(() => setRevision(r => r + 1)), [model]);
    const { tabs, mode, recording, liveFrame, annotateFrame, annotateInitiator, pendingAnnotate } = model.state;
    const interactive = mode === 'interactive';
    const [flashTick, setFlashTick] = React.useState(0);
    const displayRef = React.useRef(null);
    const screenRef = React.useRef(null);
    const toolbarRef = React.useRef(null);
    const viewportMainRef = React.useRef(null);
    const browserChromeRef = React.useRef(null);
    const interactiveBtnRef = React.useRef(null);
    const moveThrottleRef = React.useRef(0);
    const aspect = liveFrame && liveFrame.viewportWidth && liveFrame.viewportHeight
        ? liveFrame.viewportWidth / liveFrame.viewportHeight
        : null;
    const [viewportRect] = useMeasureForRef(viewportMainRef);
    // Active recording hides the browser chrome so the viewport matches what's
    // being captured.
    const showBrowserChrome = recording?.phase !== 'recording';
    const windowStyle = React.useMemo(() => {
        const OUTER_MARGIN = 24;
        const chromeHeight = showBrowserChrome ? (browserChromeRef.current?.offsetHeight ?? 40) : 0;
        const availW = viewportRect.width - OUTER_MARGIN;
        const availH = viewportRect.height - OUTER_MARGIN;
        if (availW <= 0 || availH <= 0)
            return undefined;
        if (aspect === null)
            return { width: availW, height: availH };
        const screenH = availH - chromeHeight;
        let w = availW;
        let h = w / aspect;
        if (h > screenH) {
            h = screenH;
            w = h * aspect;
        }
        return { width: w, height: h + chromeHeight };
    }, [viewportRect, aspect, showBrowserChrome]);
    React.useEffect(() => {
        if (flashTick === 0)
            return;
        const btn = interactiveBtnRef.current;
        if (!btn)
            return;
        btn.classList.remove('flash');
        // Force a reflow so that re-adding the class restarts the animation.
        void btn.offsetWidth;
        btn.classList.add('flash');
        const timer = setTimeout(() => btn.classList.remove('flash'), 2000);
        return () => {
            clearTimeout(timer);
            btn.classList.remove('flash');
        };
    }, [flashTick]);
    React.useEffect(() => {
        if (interactive)
            interactiveBtnRef.current?.classList.remove('flash');
    }, [interactive]);
    const onSubmitAnnotations = React.useCallback(async (blob, annotations) => {
        const dataUrl = await new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = () => resolve(reader.result);
            reader.onerror = () => reject(reader.error);
            reader.readAsDataURL(blob);
        });
        const data = dataUrl.slice(dataUrl.indexOf(',') + 1);
        await model.submitAnnotation(data, annotations);
    }, [model]);
    function flashInteractiveHint() {
        setFlashTick(tick => tick + 1);
    }
    const onSaveRecording = React.useCallback(async (blob) => {
        const writable = await pickSaveWritable(`playwright-recording-${Date.now()}.webm`, 'WebM Video', 'video/webm', '.webm');
        if (!writable)
            return;
        await writable.write(blob);
        await writable.close();
        model.discardRecording();
    }, [model]);
    const onCloseAnnotate = React.useCallback(() => {
        if (annotateInitiator === 'cli')
            model.completeAnnotation();
        else
            model.cancelAnnotate();
    }, [model, annotateInitiator]);
    const selectedTab = tabs?.find(t => t.selected);
    const ready = !!selectedTab;
    function imgCoords(e) {
        const vw = liveFrame?.viewportWidth ?? 0;
        const vh = liveFrame?.viewportHeight ?? 0;
        if (!vw || !vh)
            return { x: 0, y: 0 };
        const layout = getImageLayout(displayRef.current);
        if (!layout)
            return { x: 0, y: 0 };
        return clientToViewport(layout, vw, vh, e.clientX, e.clientY);
    }
    function sendMouseEvent(method, e) {
        const { x, y } = imgCoords(e);
        model[method](x, y, BUTTONS[e.button] || 'left');
    }
    function onScreenMouseDown(e) {
        e.preventDefault();
        screenRef.current?.focus();
        if (!ready)
            return;
        if (!interactive) {
            flashInteractiveHint();
            return;
        }
        sendMouseEvent('mousedown', e);
    }
    function onScreenMouseUp(e) {
        if (!interactive)
            return;
        e.preventDefault();
        sendMouseEvent('mouseup', e);
    }
    function onScreenMouseMove(e) {
        if (!interactive)
            return;
        const now = Date.now();
        if (now - moveThrottleRef.current < 32)
            return;
        moveThrottleRef.current = now;
        const { x, y } = imgCoords(e);
        model.mousemove(x, y);
    }
    function onScreenWheel(e) {
        if (!interactive)
            return;
        e.preventDefault();
        model.wheel(e.deltaX, e.deltaY);
    }
    function onScreenKeyDown(e) {
        if (!interactive)
            return;
        e.preventDefault();
        model.keydown(e.key);
    }
    function onScreenKeyUp(e) {
        if (!interactive)
            return;
        e.preventDefault();
        model.keyup(e.key);
    }
    function onOmniboxKeyDown(e) {
        if (e.key === 'Enter') {
            const value = smartUrl(e.target.value);
            model.navigate(value);
            e.currentTarget.blur();
        }
    }
    const overlayText = selectedTab ? undefined : 'Select a session';
    const isRecording = recording?.phase === 'recording';
    const showAnnotateModal = !!annotateFrame;
    const showRecording = recording?.phase === 'stopped';
    const modeLabel = showAnnotateModal ? 'Dashboard: annotate' : isRecording ? 'Dashboard: record' : 'Dashboard';
    return (<main className={'dashboard-view' + (interactive ? ' interactive' : '')} aria-label={modeLabel}>
      {/* Toolbar */}
      <div ref={toolbarRef} className='toolbar'>
        <ToolbarButton ref={interactiveBtnRef} className='mode-toggle mode-interactive' title={interactive ? 'Disable interactive mode' : 'Enable interactive mode'} toggled={interactive} disabled={!ready} onClick={() => {
            if (interactive)
                model.toggleInteractive();
            else
                model.enterInteractive();
        }}>
          {interactive ? <LockOpenIcon /> : <LockIcon />}
        </ToolbarButton>
        <ToolbarButton className='mode-annotate' title='Annotate screenshot' disabled={!ready || !!pendingAnnotate || showAnnotateModal} onClick={() => model.enterAnnotate('user')}>
          <ScreenshotRegionIcon />
        </ToolbarButton>
        <ToolbarButton className='mode-toggle mode-record' title={isRecording ? 'Stop recording' : 'Record video'} icon='record' toggled={isRecording} disabled={!ready || showRecording} onClick={() => {
            if (isRecording)
                model.stopRecording();
            else
                model.startRecording();
        }}>
          {isRecording && <span className='mode-record-label'>Recording...</span>}
        </ToolbarButton>
      </div>

      {/* Viewport */}
      <div className='viewport-wrapper'>
        <div ref={viewportMainRef} className='viewport-main'>
          <div className='browser-window' style={windowStyle}>
            {showBrowserChrome && (<div ref={browserChromeRef} className='browser-chrome'>
                <button className='nav-btn' title='Back' aria-disabled={!interactive || undefined} onClick={() => {
                if (!interactive) {
                    flashInteractiveHint();
                    return;
                }
                model.back();
            }}>
                  <ChevronLeftIcon />
                </button>
                <button className='nav-btn' title='Forward' aria-disabled={!interactive || undefined} onClick={() => {
                if (!interactive) {
                    flashInteractiveHint();
                    return;
                }
                model.forward();
            }}>
                  <ChevronRightIcon />
                </button>
                <button className='nav-btn' title='Reload' aria-disabled={!interactive || undefined} onClick={() => {
                if (!interactive) {
                    flashInteractiveHint();
                    return;
                }
                model.reload();
            }}>
                  <ReloadIcon />
                </button>
                <div className='omnibox-wrap'>
                  <input id='omnibox' className='omnibox' type='text' placeholder='Search or enter URL' spellCheck={false} autoComplete='off' value={selectedTab?.url} onChange={() => { }} onKeyDown={e => {
                if (!interactive)
                    return;
                onOmniboxKeyDown(e);
            }} onFocus={e => {
                if (!interactive) {
                    flashInteractiveHint();
                    e.target.blur();
                    return;
                }
                e.target.select();
            }} aria-disabled={!interactive || undefined} readOnly={!interactive}/>
                </div>
              </div>)}
            <div ref={screenRef} className='screen' tabIndex={0} style={{ display: liveFrame ? '' : 'none' }} onMouseDown={onScreenMouseDown} onMouseUp={onScreenMouseUp} onMouseMove={onScreenMouseMove} onWheel={onScreenWheel} onKeyDown={onScreenKeyDown} onKeyUp={onScreenKeyUp} onContextMenu={e => e.preventDefault()}>
              <img ref={displayRef} id='display' className='display' alt='screencast' src={liveFrame ? 'data:image/jpeg;base64,' + liveFrame.data : undefined}/>
            </div>
            {overlayText && <div className={'screen-overlay' + (liveFrame ? ' has-frame' : '')}><span>{overlayText}</span></div>}
          </div>
        </div>
      </div>

      {showAnnotateModal && annotateFrame && (<AnnotateModal frame={annotateFrame} showSubmit={annotateInitiator === 'cli'} onSubmit={onSubmitAnnotations} onClose={onCloseAnnotate}/>)}

      {showRecording && recording?.phase === 'stopped' && (<Recording blob={recording.blob} blobUrl={recording.blobUrl} onSave={onSaveRecording} onClose={() => model.discardRecording()}/>)}
    </main>);
};
//# sourceMappingURL=dashboard.js.map