/**
 * 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 * as React from 'react';
import './networkResourceDetails.css';
import { TabbedPane } from '@web/components/tabbedPane';
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
import { ToolbarButton } from '@web/components/toolbarButton';
import { generateCurlCommand, generateFetchCall } from '../third_party/devtools';
import { CopyToClipboardTextButton } from './copyToClipboard';
import { getAPIRequestCodeGen } from './codegen';
import { isJsonMimeType, isXmlMimeType } from '@isomorphic/mimeType';
import { useAsyncMemo, useSetting } from '@web/uiUtils';
import { msToString } from '@isomorphic/formatUtils';
import { useTraceModel } from './traceModelContext';
import { Expandable } from '@web/components/expandable';
import { Toolbar } from '@web/components/toolbar';
export const NetworkResourceDetails = ({ resource, sdkLanguage, startTimeOffset, onClose }) => {
    const [selectedTab, setSelectedTab] = React.useState('headers');
    const model = useTraceModel();
    const requestBody = useAsyncMemo(async () => {
        if (model && resource.request.postData) {
            const requestContentTypeHeader = resource.request.headers.find(q => q.name.toLowerCase() === 'content-type');
            const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : '';
            if (resource.request.postData._sha1) {
                const response = await fetch(model.createRelativeUrl(`sha1/${resource.request.postData._sha1}`));
                return { text: await response.text(), mimeType: requestContentType };
            }
            else {
                return { text: resource.request.postData.text, mimeType: requestContentType };
            }
        }
        else {
            return null;
        }
    }, [resource], null);
    return <TabbedPane leftToolbar={[<ToolbarButton key='close' icon='close' title='Close' onClick={onClose}/>]} rightToolbar={[<CopyDropdown key='dropdown' requestBody={requestBody} resource={resource} sdkLanguage={sdkLanguage}/>]} tabs={[
            {
                id: 'headers',
                title: 'Headers',
                render: () => <HeadersTab resource={resource} startTimeOffset={startTimeOffset}/>,
            },
            {
                id: 'payload',
                title: 'Payload',
                render: () => <PayloadTab resource={resource} requestBody={requestBody}/>,
            },
            {
                id: 'response',
                title: 'Response',
                render: () => <ResponseTab resource={resource}/>,
            },
        ]} selectedTab={selectedTab} setSelectedTab={setSelectedTab}/>;
};
const CopyDropdown = ({ resource, sdkLanguage, requestBody }) => {
    const model = useTraceModel();
    const copiedDescription = <><span className='codicon codicon-check' style={{ marginRight: '5px' }}/> Copied </>;
    const copyAsPlaywright = async () => getAPIRequestCodeGen(sdkLanguage).generatePlaywrightRequestCall(resource.request, requestBody?.text);
    return (<div className='copy-request-dropdown'>
      <ToolbarButton className='copy-request-dropdown-toggle'>
        <span className='codicon codicon-copy' style={{ marginRight: '5px' }}/>
        Copy request
        <span className='codicon codicon-chevron-down' style={{ marginLeft: '5px' }}/>
      </ToolbarButton>

      <div className='copy-request-dropdown-menu'>
        <CopyToClipboardTextButton description='Copy as cURL' copiedDescription={copiedDescription} value={() => generateCurlCommand(model, resource)}/>
        <CopyToClipboardTextButton description='Copy as Fetch' copiedDescription={copiedDescription} value={() => generateFetchCall(model, resource)}/>
        <CopyToClipboardTextButton description='Copy as Playwright' copiedDescription={copiedDescription} value={copyAsPlaywright}/>
      </div>
    </div>);
};
const FormatToggleButton = ({ toggled, error, onToggle }) => {
    return <ToolbarButton icon='json' title='Pretty print' toggled={toggled} errorBadge={error ? 'Formatting failed' : undefined} onClick={e => {
            e.stopPropagation();
            onToggle();
        }}/>;
};
const ExpandableSection = ({ title, data, showCount, children, titleChildren, className }) => {
    const [expanded, setExpanded] = useSetting(`trace-viewer-network-details-${title.replaceAll(' ', '-')}`, true);
    return <Expandable expanded={expanded} setExpanded={setExpanded} expandOnTitleClick title={<>
        <span className='network-request-details-header'>{title}
          {showCount && <span className='network-request-details-header-count'> × {data?.length ?? 0}</span>}
        </span>
        {titleChildren}
      </>} className={className}>
    {data && <table className='network-request-details-table'>
      <tbody>
        {data.map(({ name, value }, index) => (value !== null &&
                (<tr key={index}>
            <td>{name}</td>
            <td>{value}</td>
          </tr>)))}
      </tbody>
    </table>}
    {children}
  </Expandable>;
};
const HeadersTab = ({ resource, startTimeOffset }) => {
    const generalData = React.useMemo(() => Object.entries({
        'URL': resource.request.url,
        'Method': resource.request.method,
        'Status Code': resource.response.status === -1 ? 'canceled' : resource.response.status > 0 && <span className={statusClass(resource.response.status)}> {resource.response.status} {resource.response.statusText}</span>,
        'Start': msToString(startTimeOffset),
        'Duration': msToString(resource.time),
    }).map(([name, value]) => ({ name, value })), [resource, startTimeOffset]);
    return <div className='vbox network-request-details-tab'>
    <ExpandableSection title='General' data={generalData}/>
    <ExpandableSection title='Request Headers' showCount data={resource.request.headers}/>
    <ExpandableSection title='Response Headers' showCount data={resource.response.headers}/>
  </div>;
};
const PayloadTab = ({ resource, requestBody }) => {
    const [showFormatted, setShowFormatted] = useSetting('trace-viewer-network-details-show-formatted-payload', true);
    const hasQueryString = resource.request.queryString.length > 0;
    const hasRequestBody = !!(requestBody || resource.request.postData);
    const formatResult = useFormattedBody(requestBody, showFormatted);
    return <div className='vbox network-request-details-tab'>
    {!hasQueryString && !hasRequestBody && <em className='network-request-no-payload'>No payload for this request.</em>}
    {hasQueryString && <ExpandableSection title='Query String Parameters' showCount data={resource.request.queryString}/>}
    {requestBody && <ExpandableSection title='Request Body' className='network-request-request-body' titleChildren={<>
        <div style={{ margin: 'auto' }}></div>
        <FormatToggleButton toggled={showFormatted} error={formatResult.error} onToggle={() => setShowFormatted(!showFormatted)}/>
      </>}>
      <CodeMirrorWrapper text={formatResult.text} mimeType={requestBody.mimeType} readOnly lineNumbers={true}/>
    </ExpandableSection>}
  </div>;
};
const ResponseTab = ({ resource }) => {
    const model = useTraceModel();
    const [responseBody, setResponseBody] = React.useState(null);
    React.useEffect(() => {
        const readResources = async () => {
            if (model && resource.response.content._sha1) {
                const useBase64 = resource.response.content.mimeType.includes('image');
                const isFont = resource.response.content.mimeType.includes('font');
                const response = await fetch(model.createRelativeUrl(`sha1/${resource.response.content._sha1}`));
                if (useBase64) {
                    const blob = await response.blob();
                    const reader = new FileReader();
                    const eventPromise = new Promise(f => reader.onload = f);
                    reader.readAsDataURL(blob);
                    setResponseBody({ dataUrl: (await eventPromise).target.result });
                }
                else if (isFont) {
                    const font = await response.arrayBuffer();
                    setResponseBody({ font });
                }
                else {
                    setResponseBody({ text: await response.text(), mimeType: resource.response.content.mimeType });
                }
            }
            else {
                setResponseBody(null);
            }
        };
        readResources();
    }, [resource, model]);
    const [showFormattedResponse, setShowFormattedResponse] = useSetting('trace-viewer-network-details-show-formatted-response', true);
    const formatResult = useFormattedBody(responseBody, showFormattedResponse);
    return <div className='vbox network-request-details-tab'>
    {!resource.response.content._sha1 && <div>Response body is not available for this request.</div>}
    {responseBody && responseBody.font && <FontPreview font={responseBody.font}/>}
    {responseBody && responseBody.dataUrl && <div><img draggable='false' src={responseBody.dataUrl}/></div>}
    {responseBody && responseBody.text !== undefined && <div className='vbox network-response-body'>
      <CodeMirrorWrapper text={formatResult.text} mimeType={responseBody.mimeType} readOnly lineNumbers={true}/>
      <Toolbar noShadow={true} noMinHeight={true} className='network-response-toolbar'>
        <div style={{ margin: 'auto' }}></div>
        <FormatToggleButton toggled={showFormattedResponse} error={formatResult.error} onToggle={() => setShowFormattedResponse(!showFormattedResponse)}/>
      </Toolbar>
    </div>}
  </div>;
};
const FontPreview = ({ font }) => {
    const [isError, setIsError] = React.useState(false);
    React.useEffect(() => {
        let fontFace;
        try {
            // note: constant font family name will lead to bugs
            // when displaying two font previews.
            fontFace = new FontFace('font-preview', font);
            if (fontFace.status === 'loaded')
                document.fonts.add(fontFace);
            if (fontFace.status === 'error')
                setIsError(true);
        }
        catch {
            setIsError(true);
        }
        return () => {
            document.fonts.delete(fontFace);
        };
    }, [font]);
    if (isError)
        return <div className='network-font-preview-error'>Could not load font preview</div>;
    return <div className='network-font-preview'>
    ABCDEFGHIJKLM<br />
    NOPQRSTUVWXYZ<br />
    abcdefghijklm<br />
    nopqrstuvwxyz<br />
    1234567890
  </div>;
};
function statusClass(statusCode) {
    if (statusCode < 300 || statusCode === 304)
        return 'green-circle';
    if (statusCode < 400)
        return 'yellow-circle';
    return 'red-circle';
}
const kInlineTagPattern = /<[^>]+>[^<]*<\//;
function formatXml(xml, indent = '  ') {
    let depth = 0;
    const lines = [];
    const tokens = xml.replace(/>\s*</g, '>\n<').split('\n');
    for (const token of tokens) {
        const trimmed = token.trim();
        if (!trimmed)
            continue;
        if (trimmed.startsWith('</')) {
            depth = Math.max(depth - 1, 0);
            lines.push(indent.repeat(depth) + trimmed);
        }
        else if (trimmed.endsWith('/>') || trimmed.startsWith('<?') || kInlineTagPattern.test(trimmed)) {
            lines.push(indent.repeat(depth) + trimmed);
        }
        else if (trimmed.startsWith('<')) {
            lines.push(indent.repeat(depth) + trimmed);
            depth++;
        }
        else {
            lines.push(indent.repeat(depth) + trimmed);
        }
    }
    return lines.join('\n');
}
function formatBody(body, contentType) {
    if (!body.trim() || !contentType)
        return body;
    if (isJsonMimeType(contentType))
        return JSON.stringify(JSON.parse(body), null, 2);
    if (isXmlMimeType(contentType))
        return formatXml(body);
    if (contentType.includes('application/x-www-form-urlencoded'))
        return decodeURIComponent(body);
    return body;
}
const useFormattedBody = (body, showFormatted) => {
    return React.useMemo(() => {
        if (body?.text === undefined)
            return { text: '' };
        if (!showFormatted)
            return { text: body.text };
        try {
            return { text: formatBody(body.text, body.mimeType) };
        }
        catch {
            return { text: body.text, error: true };
        }
    }, [body, showFormatted]);
};
//# sourceMappingURL=networkResourceDetails.js.map