/*
 * Copyright 2020 IBM 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.
 */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import * as React from 'react';
import * as prettyPrintDuration from 'pretty-ms';
import { i18n } from '@kui-shell/core';
import { Icons, Loading } from '@kui-shell/plugin-client-common';
import cssForValue from '../css-for-value';
import { getCommandFromArgs } from '../../util/util';
import TrafficLight from '../../model/traffic-light';
import { HYSTERESIS } from './ContainerCommon';
import { doExecWithPty } from '../../../controller/kubectl/exec';
import { isEvent, isCrudableKubeResource, isNamespaced } from '../../model/resource';
import '../../../../web/scss/components/Events.scss';
const strings = i18n('plugin-kubectl');
const strings2 = i18n('plugin-kubectl', 'events');
/** Interval in milliseconds with which we update the "ago" timestamps */
const UPDATE_INTERVAL = 60 * 1000;
/** Parse a stream of JSON-formatted bits into a stream of ObservedEvent */
function StreamingJSONParser(cb) {
    let escaping = false;
    let inQuotes = false;
    let depth = 0;
    let bundle = '';
    return (_) => {
        const data = _.toString();
        for (let idx = 0; idx < data.length; idx++) {
            const ch = data.charAt(idx);
            const escaped = escaping;
            escaping = false;
            bundle += ch;
            if (!inQuotes && ch === '{') {
                depth++;
            }
            if (!escaped && ch === '"') {
                inQuotes = !inQuotes;
            }
            if (!escaped && ch === '\\') {
                escaping = true;
            }
            if (!inQuotes && ch === '}') {
                if (--depth === 0) {
                    const event = JSON.parse(bundle);
                    // parse the lastTimestamp ahead of time
                    const oevent = Object.assign(event, {
                        lastTimestamp: new Date(event.lastTimestamp).getTime()
                    });
                    cb(oevent);
                    bundle = '';
                }
            }
        }
    };
}
class Events extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            now: this.now(),
            timer: setInterval(this.updateNow.bind(this), UPDATE_INTERVAL),
            job: undefined,
            isLive: false,
            waitingForHysteresis: false,
            events: []
        };
        this.initStream();
    }
    now() {
        return new Date().getTime();
    }
    updateNow() {
        this.setState({ now: this.now() });
    }
    componentWillUnmount() {
        clearInterval(this.state.timer);
        if (this.state.job) {
            this.state.job.abort();
        }
    }
    /** Toolbar text to display */
    toolbarText(status) {
        return {
            type: status === 'Live' ? 'info' : status === 'Paused' ? 'warning' : 'error',
            text: strings2(status === 'Live'
                ? 'Events are live streaming'
                : status === 'Paused'
                    ? 'Event streaming is paused'
                    : status === 'Stopped'
                        ? 'Event streaming stopped'
                        : 'Event streaming stopped abnormally')
        };
    }
    toolbarButtonsForError(status) {
        if (status === 'Error') {
            return [
                {
                    mode: 'retry-streaming',
                    label: strings('Retry'),
                    kind: 'view',
                    icon: React.createElement(Icons, { icon: "Retry", onClick: () => this.initStream() }),
                    command: () => { } // eslint-disable-line @typescript-eslint/no-empty-function
                }
            ];
        }
        else {
            return [];
        }
    }
    toolbarButtonsForStreaming(status) {
        const isLive = status === 'Live';
        return [
            {
                mode: 'toggle-streaming',
                label: isLive ? strings('Pause Streaming') : strings('Resume Streaming'),
                kind: 'view',
                icon: React.createElement(Icons, { icon: isLive ? 'Pause' : 'Play' }),
                command: this.toggleStreaming.bind(this, !isLive)
            }
        ];
    }
    /** Toolbar buttons to display */
    toolbarButtons(status) {
        return this.toolbarButtonsForError(status).concat(this.toolbarButtonsForStreaming(status));
    }
    /** Update Toolbar text and Toolbar buttons. */
    updateToolbar(status) {
        this.props.toolbarController.willUpdateToolbar(this.toolbarText(status), this.toolbarButtons(status), true // replace default buttons
        );
    }
    /** Update our State to reflect the new ObservedEvents. */
    observe(newlyObservedEvents) {
        this.setState(curState => ({
            events: curState.events.concat(newlyObservedEvents).sort((a, b) => b.lastTimestamp - a.lastTimestamp)
        }));
    }
    /** The part of toggleStreaming that deals with PTY flow control. */
    doFlowControl(desiredStateIsLive = this.state.isLive) {
        if (this.state.job) {
            if (desiredStateIsLive) {
                this.state.job.xon();
            }
            else {
                this.state.job.xoff();
            }
        }
    }
    /** Handler for Pause/Play. */
    toggleStreaming(desiredState) {
        if (this.state.isLive !== desiredState) {
            this.doFlowControl(desiredState);
            this.updateToolbar(desiredState ? 'Live' : 'Paused');
            this.setState(curState => {
                if (curState.isLive !== desiredState) {
                    return { isLive: desiredState };
                }
            });
        }
    }
    /** Initialize the event stream */
    initStream() {
        const args = this.command({
            /** prior to PTY initialization, we provide a streaming consumer */
            onInit: () => {
                // this is our streaming consumer
                return StreamingJSONParser(event => this.observe([event]));
            },
            onReady: (job) => {
                this.updateToolbar('Live');
                setTimeout(() => this.setState({ waitingForHysteresis: false }), HYSTERESIS);
                this.setState({ isLive: true, job, waitingForHysteresis: true });
            },
            onExit: (exitCode) => {
                console.error('event stream exited with code', exitCode);
                this.updateToolbar(exitCode === 0 ? 'Stopped' : 'Error');
            }
        });
        doExecWithPty(args);
    }
    command(execOptions) {
        const { args, resource } = this.props;
        // limit events to those intersecting with the giving resource
        const filter = `involvedObject.apiVersion=${resource.apiVersion},involvedObject.kind=${resource.kind},involvedObject.name=${resource.metadata.name},involvedObject.namespace=${resource.metadata.namespace}`;
        // this is the command that will fetch the events table; we specify a watchable table
        // remove the OBJECT column, since we're filtering down to one object
        const argv = [
            getCommandFromArgs(args),
            'get',
            'events',
            '--field-selector',
            filter,
            '-n',
            resource.metadata.namespace,
            Events.format,
            '--watch'
        ];
        const argvNoOptions = argv.filter(_ => _[0] !== '-');
        const parsedOptions = {
            n: resource.metadata.namespace,
            'field-selector': filter,
            watch: true
        };
        const command = argv.join(' ');
        return Object.assign({}, this.props.args, {
            REPL: this.props.repl,
            command,
            argv,
            argvNoOptions,
            parsedOptions,
            execOptions
        });
    }
    /** Render the events in the case we not yet finished initializing. */
    notDoneLoading() {
        return React.createElement(Loading, null);
    }
    /** Render the events in the case we no logs to show. */
    nothingToShow() {
        return React.createElement("div", { className: "kui--hero-text" }, strings2('No events'));
    }
    render() {
        if (!this.state.job || this.state.waitingForHysteresis) {
            return this.notDoneLoading();
        }
        else if (!this.state.events || this.state.events.length === 0) {
            return this.nothingToShow();
        }
        return (React.createElement("div", { className: "scrollable scrollable-auto" },
            React.createElement("div", { className: "bx--data-table kui--kubectl-events-grid" }, this.state.events.map((event, idx) => (React.createElement(EventUI, Object.assign({}, event, { now: this.state.now, key: idx }, this.props)))))));
    }
}
Events.format = '-o json';
/** Render one event */
class EventUI extends React.PureComponent {
    age() {
        const { lastTimestamp, now } = this.props;
        const delta = lastTimestamp - now;
        let ago = '0';
        try {
            ago = strings2('ago', prettyPrintDuration(Math.max(1, -delta), { compact: true }));
        }
        catch (err) {
            console.error('error parsing lastTimestamp', lastTimestamp, err);
        }
        return React.createElement("span", { className: "kui--kubectl-event-record-age smaller-text sub-text no-wrap left-pad" }, ago);
    }
    reason() {
        const { reason } = this.props;
        return (React.createElement("div", { "data-tag": "badge" },
            React.createElement("span", { title: reason, "data-tag": "badge-circle", className: cssForValue[reason] }),
            React.createElement("span", { className: "kui--cell-inner-text smaller-text" }, reason)));
    }
    message() {
        return React.createElement("pre", { className: "kui--kubectl-event-record-message pre-wrap break-all" }, this.props.message);
    }
    source() {
        const { args, repl, source: { component, host } } = this.props;
        const onClick = () => {
            const cmd = `${getCommandFromArgs(args)} get node ${host} -o yaml`;
            repl.pexec(cmd);
        };
        return (React.createElement("span", { className: "smaller-text" },
            strings2('Generated by component X on host', component),
            React.createElement("a", { className: "small-left-pad", href: "#", onClick: onClick }, host)));
    }
    clickForDetails() {
        const onClick = () => {
            const { args, repl, metadata: { name, namespace } } = this.props;
            const cmd = `${getCommandFromArgs(args)} get event ${name} -n ${namespace} -o yaml`;
            repl.pexec(cmd);
        };
        return (React.createElement("a", { className: "smaller-text", href: "#", onClick: onClick }, strings2('Details')));
    }
    render() {
        return (React.createElement("div", { className: "kui--kubectl-event-record smaller-text" },
            React.createElement("div", { className: "flex-layout" },
                React.createElement("span", { className: "flex-fill" }, this.reason()),
                this.age()),
            this.source(),
            React.createElement("div", { className: "flex-layout full-height flex-align-stretch top-pad" },
                React.createElement("div", { className: "flex-layout" },
                    React.createElement("span", { className: "flex-fill" }, this.message()),
                    this.clickForDetails()))));
    }
}
/**
 * Extract the events
 *
 */
function content({ REPL }, resource, args) {
    return __awaiter(this, void 0, void 0, function* () {
        return {
            react: function LogsProvider(toolbarController) {
                return React.createElement(Events, { repl: REPL, resource: resource, args: args, toolbarController: toolbarController });
            }
        };
    });
}
/**
 * @return whether the given resource might possibly have events;
 * since Events never have Events, we can exclude those always
 *
 */
function hasEvents(resource) {
    return isCrudableKubeResource(resource) && !isEvent(resource) && isNamespaced(resource);
}
/**
 * Add a Events mode button to the given modes model, if called for by
 * the given resource.
 *
 */
export const eventsMode = {
    when: hasEvents,
    mode: {
        mode: 'events',
        label: strings('events'),
        content
    }
};
export const eventsBadge = {
    when: isEvent,
    badge: (event) => {
        const cssFromReason = cssForValue[event.reason];
        return {
            title: cssFromReason ? event.reason : event.type,
            css: cssFromReason ||
                (event.type === 'Error' ? TrafficLight.Red : event.type === 'Warning' ? TrafficLight.Yellow : undefined)
        };
    }
};
//# sourceMappingURL=Events.js.map