/**
 * 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 './consoleTab.css';
import { ListView } from '@web/components/listView';
import { clsx } from '@web/uiUtils';
import { msToString } from '@isomorphic/formatUtils';
import { ansi2html } from '@web/ansi2html';
import { PlaceholderPanel } from './placeholderPanel';
const ConsoleListView = (ListView);
const ansiColours = {
    log: {
        bg: 'var(--vscode-editor-background)', fg: 'var(--vscode-editor-foreground)'
    },
    warning: {
        fg: 'var(--vscode-list-warningForeground)', bg: 'var(--vscode-inputValidation-warningBackground)'
    },
    error: {
        fg: 'var(--vscode-list-errorForeground)', bg: 'var(--vscode-inputValidation-errorBackground)'
    }
};
export function useConsoleTabModel(model, selectedTime, pageId) {
    const { entries } = React.useMemo(() => {
        if (!model)
            return { entries: [] };
        const entries = [];
        function addEntry(entry) {
            const lastEntry = entries[entries.length - 1];
            const isSameAsLast = lastEntry
                && entry.browserMessage?.bodyString === lastEntry.browserMessage?.bodyString
                && entry.browserMessage?.location === lastEntry.browserMessage?.location
                && entry.browserError === lastEntry.browserError
                && entry.nodeMessage?.html === lastEntry.nodeMessage?.html
                && entry.isError === lastEntry.isError
                && entry.isWarning === lastEntry.isWarning
                && entry.timestamp - lastEntry.timestamp < 1000;
            if (isSameAsLast)
                lastEntry.repeat++;
            else
                entries.push({ ...entry, repeat: 1 });
        }
        const logEvents = [...model.events, ...model.stdio].sort((a, b) => {
            const aTimestamp = 'time' in a ? a.time : a.timestamp;
            const bTimestamp = 'time' in b ? b.time : b.timestamp;
            return aTimestamp - bTimestamp;
        });
        for (const event of logEvents) {
            if (event.type === 'console') {
                if (pageId && event.pageId !== pageId)
                    continue;
                const colours = event.messageType === 'error' ? ansiColours.error : event.messageType === 'warning' ? ansiColours.warning : ansiColours.log;
                const body = event.args && event.args.length ? format(event.args, colours) : formatAnsi(event.text, colours);
                const url = event.location.url;
                const filename = url ? url.substring(url.lastIndexOf('/') + 1) : '<anonymous>';
                const location = `${filename}:${event.location.lineNumber}`;
                addEntry({
                    browserMessage: {
                        body,
                        bodyString: event.text,
                        location,
                    },
                    isError: event.messageType === 'error',
                    isWarning: event.messageType === 'warning',
                    timestamp: event.time,
                });
            }
            if (event.type === 'event' && event.method === 'pageError') {
                if (pageId && event.pageId !== pageId)
                    continue;
                addEntry({
                    browserError: event.params.error,
                    isError: true,
                    isWarning: false,
                    timestamp: event.time,
                });
            }
            if (event.type === 'stderr' || event.type === 'stdout') {
                let html = '';
                const colours = event.type === 'stderr' ? ansiColours.error : ansiColours.log;
                if (event.text)
                    html = ansi2html(event.text.trim(), colours) || '';
                if (event.base64)
                    html = ansi2html(atob(event.base64).trim(), colours) || '';
                addEntry({
                    nodeMessage: { html },
                    isError: event.type === 'stderr',
                    isWarning: false,
                    timestamp: event.timestamp,
                });
            }
        }
        return { entries };
    }, [model, pageId]);
    const filteredEntries = React.useMemo(() => {
        if (!selectedTime)
            return entries;
        return entries.filter(entry => entry.timestamp >= selectedTime.minimum && entry.timestamp <= selectedTime.maximum);
    }, [entries, selectedTime]);
    return { entries: filteredEntries };
}
export const ConsoleTab = ({ consoleModel, boundaries, onEntryHovered, onAccepted }) => {
    if (!consoleModel.entries.length)
        return <PlaceholderPanel text='No console entries'/>;
    return <div className='console-tab'>
    <ConsoleListView name='console' onAccepted={onAccepted} onHighlighted={entry => onEntryHovered?.(entry ? consoleModel.entries.indexOf(entry) : undefined)} items={consoleModel.entries} isError={entry => entry.isError} isWarning={entry => entry.isWarning} render={entry => {
            const timestamp = msToString(entry.timestamp - boundaries.minimum);
            const timestampElement = <span className='console-time'>{timestamp}</span>;
            const errorSuffix = entry.isError ? 'status-error' : entry.isWarning ? 'status-warning' : 'status-none';
            const statusElement = entry.browserMessage || entry.browserError ? <span className={clsx('codicon', 'codicon-browser', errorSuffix)} title='Browser message'></span> : <span className={clsx('codicon', 'codicon-file', errorSuffix)} title='Runner message'></span>;
            let locationText;
            let messageBody;
            let messageInnerHTML;
            let messageStack;
            const { browserMessage, browserError, nodeMessage } = entry;
            if (browserMessage) {
                locationText = browserMessage.location;
                messageBody = browserMessage.body;
            }
            if (browserError) {
                const { error: errorObject, value } = browserError;
                if (errorObject) {
                    messageBody = errorObject.message;
                    messageStack = errorObject.stack;
                }
                else {
                    messageBody = String(value);
                }
            }
            if (nodeMessage)
                messageInnerHTML = nodeMessage.html;
            return <div className='console-line'>
          {timestampElement}
          {statusElement}
          {locationText && <span className='console-location'>{locationText}</span>}
          {entry.repeat > 1 && <span className='console-repeat'>{entry.repeat}</span>}
          {messageBody && <span className='console-line-message'>{messageBody}</span>}
          {messageInnerHTML && <span className='console-line-message' dangerouslySetInnerHTML={{ __html: messageInnerHTML }}></span>}
          {messageStack && <div className='console-stack'>{messageStack}</div>}
        </div>;
        }}/>
  </div>;
};
function format(args, colours) {
    if (args.length === 1)
        return formatAnsi(args[0].preview, colours);
    const hasMessageFormat = typeof args[0].value === 'string' && args[0].value.includes('%');
    const messageFormat = hasMessageFormat ? args[0].value : '';
    const tail = hasMessageFormat ? args.slice(1) : args;
    let argIndex = 0;
    const regex = /%([%sdifoOc])/g;
    let match;
    const formatted = [];
    let tokens = [];
    formatted.push(<span key={formatted.length + 1}>{tokens}</span>);
    let formatIndex = 0;
    while ((match = regex.exec(messageFormat)) !== null) {
        const text = messageFormat.substring(formatIndex, match.index);
        tokens.push(<span key={tokens.length + 1}>{text}</span>);
        formatIndex = match.index + 2;
        const specifier = match[0][1];
        if (specifier === '%') {
            tokens.push(<span key={tokens.length + 1}>%</span>);
        }
        else if (specifier === 's' || specifier === 'o' || specifier === 'O' || specifier === 'd' || specifier === 'i' || specifier === 'f') {
            const value = tail[argIndex++];
            const styleObject = {};
            if (typeof value?.value !== 'string')
                styleObject['color'] = 'var(--vscode-debugTokenExpression-number)';
            tokens.push(<span key={tokens.length + 1} style={styleObject}>{value?.preview || ''}</span>);
        }
        else if (specifier === 'c') {
            tokens = [];
            const format = tail[argIndex++];
            const styleObject = format ? parseCSSStyle(format.preview) : {};
            formatted.push(<span key={formatted.length + 1} style={styleObject}>{tokens}</span>);
        }
    }
    if (formatIndex < messageFormat.length)
        tokens.push(<span key={tokens.length + 1}>{messageFormat.substring(formatIndex)}</span>);
    for (; argIndex < tail.length; argIndex++) {
        const value = tail[argIndex];
        const styleObject = {};
        if (tokens.length)
            tokens.push(<span key={tokens.length + 1}> </span>);
        if (typeof value?.value !== 'string')
            styleObject['color'] = 'var(--vscode-debugTokenExpression-number)';
        tokens.push(<span key={tokens.length + 1} style={styleObject}>{value?.preview || ''}</span>);
    }
    return formatted;
}
function formatAnsi(text, colours) {
    // eslint-disable-next-line react/jsx-key
    return [<span dangerouslySetInnerHTML={{ __html: ansi2html(text.trim(), colours) }}></span>];
}
function parseCSSStyle(cssFormat) {
    try {
        const styleObject = {};
        const cssProperties = cssFormat.split(';');
        for (const token of cssProperties) {
            const property = token.trim();
            if (!property)
                continue;
            let [key, value] = property.split(':');
            key = key.trim();
            value = value.trim();
            if (!supportProperty(key))
                continue;
            // cssProperties are background-color, JSDom ones are backgroundColor
            const cssKey = key.replace(/-([a-z])/g, g => g[1].toUpperCase());
            styleObject[cssKey] = value;
        }
        return styleObject;
    }
    catch (e) {
        return {};
    }
}
function supportProperty(cssKey) {
    const prefixes = ['background', 'border', 'color', 'font', 'line', 'margin', 'padding', 'text'];
    return prefixes.some(p => cssKey.startsWith(p));
}
//# sourceMappingURL=consoleTab.js.map