/*
 * Copyright 2021 The Kubernetes Authors
 *
 * 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 Debug from 'debug';
import React from 'react';
import { v4 as uuid } from 'uuid';
import { Events, i18n, isError, isMixedResponse, isTable, isXtermResponse, hasSourceReferences, pexecInCurrentTab } from '@kui-shell/core';
import StreamingConsumer from './StreamingConsumer';
import Timer from './Timer';
import Spinner from './Spinner';
import Scalar from '../../../Content/Scalar';
import { MutabilityContext, MutabilityStateOffline } from '../../../Client/MutabilityContext';
import Tooltip from '../../../spi/Tooltip';
import TwoFaceIcon from '../../../spi/Icons/TwoFaceIcon';
import { emitGetCodeBlockReadiness, onCodeBlockReadiness, offCodeBlockReadiness } from './CodeBlockEvents';
import { linkUpdateChannel, linkGetChannel } from '../../../Content/LinkStatus';
import { ProgressStepper, ProgressStep } from '../../../Content/ProgressStepper';
const BadgeUI = React.lazy(() => import('../../../spi/Tag'));
const SourceRef = React.lazy(() => import('../SourceRef'));
const CodeSnippet = React.lazy(() => import('../../../spi/CodeSnippet'));
const ExpandableSection = React.lazy(() => import('../../../spi/ExpandableSection'));
const debug = Debug('plugin-client-common/components/Views/Terminal/Block/CodeBlock');
const strings = i18n('plugin-client-common');
/**
 * Thin wrapper over the Badge component that places the badge in the
 * top left of the surrounding CodeBlock component.
 */
function Badge(props) {
    return (React.createElement("span", { className: "kui--code-block-actions", "data-align": props.position },
        React.createElement(BadgeUI, null, props.children)));
}
export default class CodeBlock extends StreamingConsumer {
    constructor(props) {
        super(props);
        this.mounted = false;
        this.cleaners = [];
        /**
         * Re-execute command upon receipt of an event on the specified
         * `this.props.watch` channel.
         *
         */
        this.onWatchEvent = () => this._onRun();
        /** Is this code block ready for execution? E.g. perhaps the prereqs have yet to be satisfied. */
        this._updateReadiness = (ready) => {
            if (this.mounted) {
                this.setState({ ready });
            }
        };
        /** Updates coming from the CodeSnippet component */
        this.onContentChange = (codeSnippetValue) => {
            if (this.mounted) {
                this.setState({ codeSnippetValue });
            }
        };
        /**
         * We are in offline mode, we have sample output, and the user
         * clicked the Run button. We only need to shift to showing the
         * original status of the code block execution.
         */
        this._onRunUsingSampleOutput = () => {
            if (this.props.response && this.mounted) {
                this.setState({ execution: this.props.rawStatus });
            }
        };
        this._onRun = () => {
            this.onRun(this.state.value);
        };
        this._onCleanup = () => __awaiter(this, void 0, void 0, function* () {
            if (this.mounted) {
                yield this.onRun(this.props.cleanup);
                this.setState({ validated: false, execution: 'not-yet' });
            }
        });
        this.state = CodeBlock.getDerivedStateFromProps(props);
    }
    componentDidMount() {
        this.mounted = true;
        this.initLinkEvents();
        this.initWatchEvents();
        this.initCodeBlockEvents();
        this.doValidate();
        if (this.props.executeImmediately && !this.props.response) {
            setTimeout(this._onRun);
        }
    }
    componentWillUnmount() {
        this.mounted = false;
        this.cleaners.forEach(_ => _());
    }
    componentDidUpdate(prevProps) {
        if (prevProps.validate !== this.props.validate) {
            this.doValidate();
        }
    }
    doValidate() {
        if (this.props.blockId && this.props.validate) {
            setTimeout(() => __awaiter(this, void 0, void 0, function* () {
                try {
                    // .toString() in case of e.g. `validate: true` which yaml
                    // parsers pass to us as a boolean
                    this.emitLinkStatus('processing');
                    yield pexecInCurrentTab(this.props.validate.toString(), undefined, true, true);
                    this.emitLinkStatus('done');
                    if (this.mounted) {
                        this.setState({ validated: true });
                    }
                }
                catch (err) {
                    this.emitLinkStatus('not-yet');
                    if (this.mounted) {
                        this.setState({ validated: false });
                    }
                }
            }), 1000);
        }
    }
    /** Is this code block ready for execution? E.g. perhaps the prereqs have yet to be satisfied. */
    initCodeBlockEvents() {
        onCodeBlockReadiness(this.props.blockId, this._updateReadiness);
        this.cleaners.push(() => offCodeBlockReadiness(this.props.blockId, this._updateReadiness));
        emitGetCodeBlockReadiness(this.props.blockId, this._updateReadiness);
    }
    initWatchEvents() {
        if (this.props.watch) {
            const channels = this.props.watch.split(/\s*,\s*/);
            debug(`initializing code block watch events on ${channels.length} channel${channels.length === 1 ? '' : 's'}: ${channels}`);
            channels.forEach(channel => {
                Events.eventChannelUnsafe.on(channel, this.onWatchEvent);
                this.cleaners.push(() => Events.eventChannelUnsafe.off(channel, this.onWatchEvent));
            });
        }
    }
    emitLinkStatus(execution = this.state.execution) {
        Events.eventChannelUnsafe.emit(linkUpdateChannel(this.props.blockId), execution === 'not-yet' || execution === 'replayed'
            ? [0, 0, 0] // indicates nothing has happened yet, in this session
            : execution === 'processing'
                ? [0, 0, 1]
                : execution === 'error'
                    ? [0, 1, 0]
                    : [1, 0, 0], this.props.blockId);
    }
    initLinkEvents() {
        if (this.props.blockId) {
            const get = linkGetChannel(this.props.blockId);
            const emit = this.emitLinkStatus.bind(this);
            Events.eventChannelUnsafe.on(get, emit);
            this.cleaners.push(() => Events.eventChannelUnsafe.off(get, emit));
        }
    }
    static getDerivedStateFromProps(props, state) {
        const usePropsValue = !state || !state.codeSnippetValue;
        if (!usePropsValue && state && state.codeSnippetValue) {
            return Object.assign(state, {
                value: state.codeSnippetValue
            });
        }
        else if (!state || state.value !== props.value || state.language !== props.language) {
            const execUUID = uuid();
            return Object.assign({
                ready: true,
                execution: props.status || 'not-yet',
                value: usePropsValue ? props.value : state.codeSnippetValue || state.value,
                language: props.language,
                validated: false
            }, StreamingConsumer.initStreamingState(execUUID));
        }
        else {
            return state;
        }
    }
    /** Are we in the middle of an execution of this code block? */
    get isExecutionInProgress() {
        return this.state.execution === 'processing' && this.state.startTime && !this.state.endTime;
    }
    get isExecutedSuccessfullyOrValidated() {
        return this.state.validated || this.state.execution === 'done';
    }
    /** UI to denote duration of the current Run */
    timer() {
        return (React.createElement(Badge, { position: "top-right" },
            React.createElement(Timer, { startTime: this.state.startTime, endTime: this.state.endTime, status: this.state.execution })));
    }
    /** UI to denote the execution status of this code block */
    status() {
        const execution = this.state.validated ? 'done' : this.state.execution;
        if (execution === 'done' || execution === 'error') {
            return (React.createElement("div", { className: "kui--code-block-actions flex-layout", "data-align": "top-left" },
                React.createElement(ProgressStepper, { className: "kui--code-block-status" },
                    React.createElement(ProgressStep, { title: "", defaultStatus: execution === 'done' ? 'success' : 'error' }))));
        }
        else if (this.props.optional) {
            return React.createElement(Badge, { position: "top-left" }, strings('Optional'));
        }
    }
    actions(showAsExecutable, butUseSampleOutputOnRun) {
        if (this.props.readonly !== true && showAsExecutable) {
            if (this.state.execution === 'processing') {
                return React.createElement(Spinner, null);
            }
            else {
                return (React.createElement(React.Fragment, null,
                    this.props.cleanup && this.isExecutedSuccessfullyOrValidated && React.createElement(Cleanup, { onCleanup: this._onCleanup }),
                    React.createElement(Run, { ready: this.state.ready, optional: this.props.optional, execution: this.state.execution, onRun: butUseSampleOutputOnRun ? this._onRunUsingSampleOutput : this._onRun })));
            }
        }
    }
    input(showAsExecutable, butUseSampleOutputOnRun) {
        return (React.createElement("div", { className: "repl-input-element-wrapper flex-layout flex-fill kui--inverted-color-context kui--relative-positioning" },
            React.createElement("div", { className: "flex-fill" },
                React.createElement(CodeSnippet, { wordWrap: "on", value: this.state.value, language: this.state.language, onContentChange: this.onContentChange, readonly: this.props.readonly !== false, tabUUID: this.props.tab ? this.props.tab.uuid : undefined })),
            this.timer(),
            this.status(),
            this.actions(showAsExecutable, butUseSampleOutputOnRun)));
    }
    sourceRef() {
        return (this.props.response &&
            isTable(this.props.response) &&
            hasSourceReferences(this.props.response) && React.createElement(SourceRef, { tab: this.props.tab, response: this.props.response }));
    }
    /**
     * If we have a command line e.g. `echo hi; echo ho`, then it would
     * be nice to smash these into a single group, so that they are
     * rendered as if they were executed together. See "semicolons
     * between commands and escape newlines" below for why this isn't
     * the case without some fixups here.
     *
     */
    combineContiguousXtermResponses() {
        if (!isMixedResponse(this.props.response)) {
            return this.props.response;
        }
        else {
            return this.props.response.reduce((A, res) => {
                if (A.length === 0) {
                    A.push(res);
                }
                else {
                    const prev = A[A.length - 1];
                    if (isXtermResponse(prev) && isXtermResponse(res)) {
                        // then we have two consecutive XtermResponses; smash them
                        // together
                        prev.rows = prev.rows.concat(res.rows);
                        if (prev.code === 0) {
                            // conservatively take non-zero exit codes over zero
                            // exit codes
                            prev.code = res.code;
                        }
                    }
                    else {
                        A.push(res);
                    }
                }
                return A;
            }, []);
        }
    }
    nonstreamingOutput() {
        const response = this.combineContiguousXtermResponses();
        return (this.state.execution !== 'processing' &&
            this.props.response && (React.createElement(Scalar, { tab: this.props.tab, execUUID: this.state.execUUID, response: response, willChangeSize: this.nope })));
    }
    get hasOutput() {
        return this.props.response || this.hasStreamingOutput();
    }
    output(mutability) {
        const content = this.hasOutput && (React.createElement(MutabilityContext.Provider, { value: this.showingReplayedOutput ? MutabilityStateOffline : mutability },
            React.createElement("div", { className: "repl-output repl-result-has-content" },
                React.createElement("div", { className: "result-vertical" },
                    React.createElement("div", { className: "repl-result" },
                        this.state.execution === 'processing' && this.streamingOutput(),
                        this.nonstreamingOutput())))));
        return this.state.execution !== 'replayed'
            ? content
            : content && (React.createElement(ExpandableSection, { className: "kui--sample-output", showMore: strings('Show Sample Output'), showLess: strings('Hide Sample Output') }, content));
    }
    onRun(cmdline) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.mounted) {
                return;
            }
            try {
                this.setState({ execution: 'processing', startTime: Date.now(), endTime: null });
                this.emitLinkStatus('processing');
                // semicolons between commands and escape newlines
                const cleanedUpCmdline = cmdline
                    .replace(/^\s*#.*$/gm, '') // strip comments
                    .replace(/([^\\])(;?)\n/g, (_, p1, p2) => `${p1}${p2 || ';'} `) // newline->semi (don't add semi when not needed)
                    .replace(/&;/g, '&'); // oof, correct for &; as just &
                const response = yield this.execWithStream(cleanedUpCmdline);
                const endTime = Date.now();
                const execution = isXtermResponse(response)
                    ? response.code === 0
                        ? 'done'
                        : 'error'
                    : isError(response)
                        ? 'error'
                        : isMixedResponse(response)
                            ? response.find(_ => (isXtermResponse(_) && _.code !== 0) || isError(_))
                                ? 'error'
                                : 'done'
                            : 'done';
                this.setState({ execution, endTime });
                this.props.onResponse(execution, response, this.props.arg1, this.props.arg2, this.props.arg3);
                this.emitLinkStatus(execution);
            }
            catch (err) {
                this.setState({ execution: 'error', endTime: Date.now() });
                this.props.onResponse('error', err, this.props.arg1, this.props.arg2, this.props.arg3);
                this.emitLinkStatus('error');
            }
        });
    }
    responseStatus() {
        if (this.state.execution === 'not-yet') {
            return "empty" /* BlockState.Empty */;
        }
        else if (this.state.execution === 'processing') {
            return "processing" /* BlockState.Processing */;
        }
        else if (this.state.execution === 'error') {
            return "error" /* BlockState.Error */;
        }
        else {
            return "valid-response" /* BlockState.ValidResponse */;
        }
    }
    get outputOnly() {
        return this.props.outputOnly === true;
    }
    /** Extract any data- properties from Props */
    dataProps() {
        return Object.entries(this.props)
            .filter(([key]) => /^data-/.test(key))
            .reduce((M, [key, value]) => {
            M[key] = value;
            return M;
        }, {});
    }
    get showingReplayedOutput() {
        return !!(this.state.execution === 'replayed' && this.hasOutput);
    }
    render() {
        return (React.createElement(MutabilityContext.Consumer, null, mutability => {
            const showAsExecutable = this.showingReplayedOutput || mutability.executable || undefined;
            const butUseSampleOutputOnRun = !mutability.executable;
            return (React.createElement("li", Object.assign({ id: this.props.id, className: `repl-block ${this.responseStatus()} ${this.props.className || ''}`, "data-is-executable": showAsExecutable, "data-is-output-only": this.outputOnly || undefined, "data-is-ready": this.state.ready || undefined, "data-is-optional": this.props.optional || undefined, "data-is-validated": this.state.validated || undefined, "data-is-sample-output": (this.hasOutput && this.state.execution === 'replayed') || undefined }, this.dataProps()),
                !this.outputOnly && this.input(showAsExecutable, butUseSampleOutputOnRun),
                !this.outputOnly && this.sourceRef(),
                this.output(mutability)));
        }));
    }
}
class ActionButton extends React.PureComponent {
    classNameOf(cls) {
        return `kui--block-action ${cls}`;
    }
    render() {
        return (React.createElement("div", { className: "flex-layout" },
            React.createElement(Tooltip, { content: this.tooltip, position: "bottom-end" },
                React.createElement(TwoFaceIcon, { a: this.icon, b: "Checkmark", delay: 4000, onClick: this.onClick, classNameB: "green-text", className: this.className }))));
    }
}
/** The Code Block Trash button */
class Cleanup extends ActionButton {
    constructor() {
        super(...arguments);
        this._className = this.classNameOf('kui--block-action-cleanup');
    }
    get className() {
        return this._className;
    }
    get tooltip() {
        return 'Clean up the effects of this code block';
    }
    get icon() {
        return 'Trash';
    }
    get onClick() {
        return this.props.onCleanup;
    }
}
/** The Code Block Run button */
class Run extends ActionButton {
    constructor() {
        super(...arguments);
        this._className = this.classNameOf('kui--block-action-run');
    }
    get className() {
        return this._className;
    }
    get tooltip() {
        return strings(!this.props.ready && this.props.execution !== 'done' && this.props.execution !== 'error'
            ? 'Execute with caution! The pre-requisites for this code block have not yet been satisfied.'
            : this.props.execution === 'done'
                ? 'Re-execute this command'
                : this.props.optional
                    ? 'Execution of this command is optional'
                    : 'Execute this command');
    }
    get icon() {
        return 'Play';
    }
    get onClick() {
        return this.props.onRun;
    }
}
//# sourceMappingURL=Inputv2.js.map