/*
 * Copyright 2020 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.
 */
import React from 'react';
import { i18n } from '@kui-shell/core/mdist/api/i18n';
import { isTable } from '@kui-shell/core/mdist/api/Table';
import { doCancel } from '@kui-shell/core/mdist/api/Internal';
import { hasSourceReferences } from '@kui-shell/core/mdist/api/Response';
import Timer from './Timer';
import Actions from './Actions';
import Spinner from './Spinner';
import onPaste from './OnPaste';
import onKeyDown from './OnKeyDown';
import onKeyPress from './OnKeyPress';
import isInViewport from '../visible';
import KuiContext from '../../../Client/context';
import { onKeyUp } from './ActiveISearch';
import whenNothingIsSelected from '../../../../util/selection';
import { endsWithBackSlash, includesBachSlash, isMultiLineHereDoc } from '../../util/multiline-input';
import { isActive, isActiveAndDifferent, isBeingRerun, isProcessingOrBeingRerun as isProcessing, isFinished, isOk, isOops, hasCommand, isEmpty, hasStartEvent, isWithCompleteEvent, isReplay, hasBeenRerun, hasUUID, hasValue } from './BlockModel';
import FancyPipeline from './FancyPipeline';
import { MutabilityContext } from '../../../Client/MutabilityContext';
const Tag = React.lazy(() => import('../../../spi/Tag'));
const TextArea = React.lazy(() => import('../../../spi/TextArea'));
const SourceRef = React.lazy(() => import('../SourceRef'));
const Icons = React.lazy(() => import('../../../spi/Icons'));
const strings = i18n('plugin-client-common');
export function isHTMLInputElement(element) {
    const input = element;
    return input && input.tagName === 'INPUT';
}
export function isHTMLTextAreaElement(element) {
    const input = element;
    return input && input.tagName === 'TEXTAREA';
}
/**
 * This helps to work around React swallowing change events when
 * setting value via ref; e.g.
 *
 *   element.value='foo'.
 *   element.dispatchEvent(new Event('input', { bubbles: true }))
 *
 */
function setNativeValue(element, value) {
    const { set: valueSetter } = Object.getOwnPropertyDescriptor(element, 'value') || {};
    const prototype = Object.getPrototypeOf(element);
    const { set: prototypeValueSetter } = Object.getOwnPropertyDescriptor(prototype, 'value') || {};
    if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
        prototypeValueSetter.call(element, value);
    }
    else if (valueSetter) {
        valueSetter.call(element, value);
    }
    else {
        throw new Error('The given element does not have a value setter');
    }
}
export class InputProvider extends React.PureComponent {
    constructor() {
        super(...arguments);
        this._cancelReEdit = this.cancelReEdit.bind(this);
    }
    /** rendered to the left of the input element */
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    status() { }
    cancelReEdit() {
        this.setState(() => {
            return {
                isReEdit: false
            };
        });
    }
    contextContent(insideBrackets = this.idx) {
        return this.state.isReEdit ? (React.createElement("a", { href: "#", className: "kui--block-action", title: strings('Cancel edit'), onClick: this._cancelReEdit },
            React.createElement(Icons, { icon: "Edit", className: "clickable" }))) : (React.createElement("span", { className: "repl-context-inner" },
            ' ',
            "[",
            insideBrackets,
            "]")); // this.props.model.cwd
    }
    /** the "xxx" part of "xxx >" of the prompt */
    promptLeft() {
        return (!this.props.noPromptContext && (React.createElement(KuiContext.Consumer, null, config => !config.noPromptContext &&
            this.props.model && (React.createElement("span", { className: "repl-context", onClick: this.props.willFocusBlock, "data-input-count": this.props.idx }, this.contextContent())))));
    }
    /** the ">" part of "xxx >" of the prompt */
    promptRight() {
        // &#x2771; "heavy right-pointing angle bracket ornament"
        // another option: &#x276f; "heavy right-pointing angle quotation mark ornament"
        const active = this.props.model && isActive(this.props.model);
        return (React.createElement(KuiContext.Consumer, null, config => (React.createElement("span", { className: "repl-prompt-righty" }, config.prompt === 'CWD' ? (React.createElement("span", { className: "clickable", onClick: () => this.props.tab.REPL.pexec(`ls ${this.props.model.cwd}`) },
            active && '[',
            this.props.model.cwd,
            active && ']')) : (config.prompt || '/')))));
    }
    isearchPrompt() {
        return React.createElement("div", { className: "repl-prompt" }, this.state.isearch.render());
    }
    normalPrompt() {
        return (React.createElement(KuiContext.Consumer, null, config => (React.createElement("div", { className: "repl-context", "data-custom-prompt": !!config.prompt || undefined, onClick: this.props.willFocusBlock, "data-input-count": this.props.idx }, config.prompt ? React.createElement("div", { className: "repl-prompt" }, this.promptRight()) : this.contextContent()))));
    }
    /** the "xxx >" prompt part of the input section */
    prompt() {
        if (this.state && this.state.isearch && this.state.prompt) {
            try {
                return this.isearchPrompt();
            }
            catch (err) {
                console.error('error rendering i-search', err);
                return this.normalPrompt();
            }
        }
        else {
            return this.normalPrompt();
        }
    }
    sourceRef() {
        return (this.props.model &&
            isWithCompleteEvent(this.props.model) &&
            isTable(this.props.model.response) &&
            hasSourceReferences(this.props.model.response) && (React.createElement(SourceRef, { tab: this.props.tab, isWidthConstrained: this.props.isWidthConstrained, response: this.props.model.response })));
    }
    get promptValue() {
        return hasValue(this.props.model)
            ? this.props.model.value
            : hasCommand(this.props.model)
                ? this.props.model.command
                : '';
    }
    get idx() {
        return this.props.displayedIdx || this.props.idx + 1;
    }
    render() {
        return (React.createElement(React.Suspense, { fallback: React.createElement("div", null) },
            React.createElement("div", { className: 'repl-input' + (this.state && this.state.isearch ? ' kui--isearch-active' : '') },
                this.prompt(),
                React.createElement("div", { className: "kui--input-and-context" },
                    this.props.children,
                    this.input(),
                    this.status()),
                this.state && this.state.tabCompletion && this.state.tabCompletion.render()),
            this.sourceRef()));
    }
}
export default class Input extends InputProvider {
    constructor(props) {
        super(props);
        this._onKeyPress = this.onKeyPress.bind(this);
        this._onKeyDown = this.onKeyDown.bind(this);
        this._onKeyUp = this.onKeyUp.bind(this);
        this._onPaste = this.onPaste.bind(this);
        /** This is the onFocus property of the active prompt */
        this._onFocus = (evt) => {
            this.props.onInputFocus && this.props.onInputFocus(evt);
            this.willFocusBlock(evt);
        };
        /** This is the onBLur property of the active prompt */
        this._onBlur = (evt) => {
            this.props.onInputBlur && this.props.onInputBlur(evt);
            const valueNotChanged = hasCommand(this.props.model) && this.state.prompt && this.props.model.command === this.state.prompt.value;
            this.setState(curState => {
                if (curState.isReEdit && valueNotChanged) {
                    return {
                        isReEdit: false,
                        prompt: undefined
                    };
                }
            });
        };
        /** This is the onClick property of the prompt for Active blocks */
        this._onClickActive = (evt) => {
            this.props.onInputClick && this.props.onInputClick(evt);
            this.willFocusBlock(evt);
        };
        /** This is the onClick property of the prompt for Finished blocks */
        this._onClickFinished = whenNothingIsSelected((evt) => {
            this.willFocusBlock(evt);
            this.setState(curState => {
                if (!curState.isReEdit) {
                    return {
                        isReEdit: true,
                        multiline: hasCommand(this.props.model) &&
                            (includesBachSlash(this.props.model.command) || isMultiLineHereDoc(this.props.model.command))
                    };
                }
            });
        });
        /** For Processing blocks, we use an overlay <input/> element. We need to manage focus, since it has pointer-events: none */
        this._restoreFocusToOverlayInput = whenNothingIsSelected((evt) => {
            ;
            evt.currentTarget.querySelector('.kui--invisible-overlay').focus();
        });
        this.state = {
            model: props.model,
            isReEdit: false,
            execUUID: hasUUID(props.model) ? props.model.execUUID : undefined,
            prompt: undefined
        };
    }
    /** @return the current value of the prompt */
    value() {
        return this.state.prompt && this.state.prompt.value;
    }
    /** @return the value to be added to the prompt */
    static valueToBeDisplayed(props) {
        return !isBeingRerun(props.model) && hasValue(props.model)
            ? props.model.value
            : hasCommand(props.model)
                ? props.model.command
                : '';
    }
    /** Owner wants us to focus on the current prompt */
    doFocus() {
        if (this.props.isFocused && this.state.prompt && document.activeElement !== this.state.prompt) {
            setTimeout(() => this.state.prompt.focus(), 50);
        }
    }
    contextContent() {
        return super.contextContent(this.showSpinnerInContext() && isProcessing(this.props.model) ? this.spinner() : undefined);
    }
    static getDerivedStateFromProps(props, state) {
        if (hasUUID(props.model)) {
            return {
                model: props.model,
                execUUID: props.model.execUUID
            };
        }
        else if (state.prompt && isActive(props.model) && state.execUUID !== undefined && !state.isearch) {
            // e.g. terminal has been cleared; we need to excise the current
            // <input/> because react aggressively caches these
            return {
                model: props.model,
                prompt: undefined,
                execUUID: undefined
            };
        }
        else if (isActiveAndDifferent(props.model, state.model)) {
            return {
                model: props.model,
                prompt: undefined,
                execUUID: undefined
            };
        }
        return state;
    }
    onKeyPress(evt) {
        if (!this.state.isearch) {
            onKeyPress.bind(this)(evt);
        }
        this.props.onInputKeyPress && this.props.onInputKeyPress(evt);
    }
    onKeyDown(evt) {
        if (!this.state.isearch) {
            onKeyDown.bind(this)(evt);
        }
        this.props.onInputKeyDown && this.props.onInputKeyDown(evt);
    }
    onKeyUp(evt) {
        onKeyUp.bind(this)(evt);
        this.props.onInputKeyUp && this.props.onInputKeyUp(evt);
    }
    onPaste(evt) {
        if (!this.state.isearch) {
            onPaste.bind(this)(evt.nativeEvent, this.props.tab, this.state.prompt);
        }
    }
    onTextAreaRef(c) {
        if (c && (!this.state.prompt || isHTMLInputElement(this.state.prompt) || this.state.isReEdit)) {
            let value = this.promptValue;
            if (isHTMLInputElement(this.state.prompt)) {
                if (endsWithBackSlash(this.state.prompt.value)) {
                    value += `${this.state.prompt.value}\n`;
                }
                else if (this.state.pasteMultiLineTexts) {
                    value += `${this.state.prompt.value}${this.state.pasteMultiLineTexts}`;
                }
                else {
                    value += this.state.prompt.value;
                }
                setNativeValue(c, value); // see the comment on the function; React swallows the event otherwise
                c.dispatchEvent(new Event('input', { bubbles: true })); // to get TextArea to adjust its height
            }
            this.setState({ prompt: c });
            if (this.props.isFocused && document.activeElement !== c) {
                c.focus();
            }
        }
        else if (c && this.props.isFocused && isInViewport(c)) {
            c.focus();
        }
    }
    onRef(c) {
        if (c && (!this.state.prompt || this.state.isReEdit)) {
            c.value = hasValue(this.props.model)
                ? this.props.model.value
                : hasCommand(this.props.model)
                    ? this.props.model.command
                    : '';
            this.setState({ prompt: c });
            if (this.props.isFocused && document.activeElement !== c) {
                c.focus();
            }
        }
        else if (c && this.props.isFocused && isInViewport(c)) {
            c.focus();
        }
    }
    willFocusBlock(evt) {
        if (this.props.willFocusBlock) {
            this.props.willFocusBlock(evt);
        }
    }
    /** This is the input overlay for Processing blocks */
    inputOverlayForProcessingBlocks(value) {
        return (React.createElement("input", { className: "kui--invisible-overlay", readOnly: true, onKeyDown: evt => {
                if (evt.key === 'c' && evt.ctrlKey) {
                    doCancel(this.props.tab, this.props._block, value);
                }
            }, ref: c => c && c.focus() }));
    }
    /** if in multi-line mode, render text area */
    multilineInput() {
        return (React.createElement("div", { className: "repl-input-element-wrapper flex-layout flex-fill", "data-is-reedit": this.state.isReEdit || undefined },
            React.createElement(TextArea, { autoResize: true, autoFocus: this.props.isFocused && isInViewport(this.props._block), autoCorrect: "off", autoComplete: "off", spellCheck: "false", autoCapitalize: "off", className: 'repl-input-element' + (this.state.isearch ? ' repl-input-hidden' : '') + ' repl-textarea', "aria-label": "Command Input", tabIndex: 1, "data-scrollback-uuid": this.props.uuid, "data-input-count": this.props.idx, onBlur: this._onBlur, onFocus: this._onFocus, onKeyPress: this._onKeyPress, onKeyDown: this._onKeyDown, innerRef: this.onTextAreaRef.bind(this) /* DO NOT MEMOIZE THIS; see https://github.com/kubernetes-sigs/kui/issues/7648 */ }),
            this.state.typeahead && React.createElement("span", { className: "kui--input-typeahead" }, this.state.typeahead)));
    }
    /** if not in multi-line mode, render an inline input */
    inlineInput() {
        return (React.createElement("div", { className: "repl-input-element-wrapper flex-layout flex-fill", "data-is-reedit": this.state.isReEdit || undefined, key: this.state.execUUID },
            React.createElement("input", { type: "text", autoFocus: this.props.isFocused && isInViewport(this.props._block), autoCorrect: "off", autoComplete: "off", spellCheck: "false", autoCapitalize: "off", className: 'repl-input-element' + (this.state.isearch ? ' repl-input-hidden' : ''), "aria-label": "Command Input", tabIndex: 1, "data-scrollback-uuid": this.props.uuid, "data-input-count": this.props.idx, onBlur: this._onBlur, onFocus: this._onFocus, onMouseDown: this.props.onInputMouseDown, onMouseMove: this.props.onInputMouseMove, onChange: this.props.onInputChange, onClick: this._onClickActive, onKeyPress: this._onKeyPress, onKeyDown: this._onKeyDown, onKeyUp: this._onKeyUp, onPaste: this._onPaste, ref: this.onRef.bind(this) /* DO NOT MEMOIZE THIS; see https://github.com/kubernetes-sigs/kui/issues/7648 */ }),
            this.state.typeahead && React.createElement("span", { className: "kui--input-typeahead" }, this.state.typeahead)));
    }
    /** @return the UI for the input part of Finished blocks (i.e. those not processing or active) */
    inputForFinishedBlock() {
        const value = Input.valueToBeDisplayed(this.props);
        const isInProgress = isProcessing(this.props.model);
        // for Processing or Done blocks, render the value as a plain div
        // for Processing, though, we will need an inputOverlay... to capture ctrl+c
        return (React.createElement(MutabilityContext.Consumer, null, mutability => (React.createElement("div", { "data-input-count": this.props.idx, className: 'repl-input-element-wrapper flex-layout flex-fill', onClick: isInProgress ? this._restoreFocusToOverlayInput : mutability.editable ? this._onClickFinished : undefined },
            this.fancyValue(value),
            value.length === 0 && React.createElement("span", { className: "kui--repl-input-element-nbsp" }, "\u00A0"),
            isInProgress && this.inputOverlayForProcessingBlocks(value),
            this.inputStatus(value)))));
    }
    /** the element that represents the command being/having been/going to be executed */
    input() {
        const active = isActive(this.props.model) || this.state.isReEdit;
        if (active) {
            if (this.state.prompt) {
                if (this.props.isFocused) {
                    if (document.activeElement !== this.state.prompt) {
                        setTimeout(() => {
                            if (isInViewport(this.state.prompt) && this.props.isFocused) {
                                this.state.prompt.focus();
                            }
                        });
                    }
                }
                else {
                    // @starpit 20210128; i think this is leftover from earlier buggier days
                    // keeping around, commented out, for a bit just in case
                    // setTimeout(() => this.state.prompt.scrollIntoView(), 10)
                }
            }
            return this.state.multiline ? this.multilineInput() : this.inlineInput();
        }
        else {
            return this.inputForFinishedBlock();
        }
    }
    /**
     * Turn the value part of the input into a fancier form,
     * e.g. rendering pipelines or command names in a better way.
     *
     */
    fancyValue(value) {
        if (!isBeingRerun(this.props.model) &&
            isWithCompleteEvent(this.props.model) &&
            this.props.model.completeEvent.pipeStages !== undefined) {
            return React.createElement(FancyPipeline, Object.assign({ REPL: this.props.tab.REPL }, this.props.model.completeEvent.pipeStages));
        }
        else if (hasStartEvent(this.props.model) && this.props.model.startEvent.pipeStages !== undefined) {
            return React.createElement(FancyPipeline, Object.assign({ REPL: this.props.tab.REPL }, this.props.model.startEvent.pipeStages));
        }
        else {
            return React.createElement("span", { className: "repl-input-element flex-fill" }, value);
        }
    }
    componentDidCatch(error, errorInfo) {
        console.error(error, errorInfo);
    }
    /** render a tag for experimental command */
    experimentalTag() {
        if (this.props.isExperimental) {
            return (React.createElement("span", { className: "kui--repl-block-right-element left-pad kui--repl-block-experimental-tag-wrapper", key: "experimental-tag" },
                React.createElement(Tag, { spanclassname: "kui--repl-block-experimental-tag", title: strings('HoverExperimentalTag'), type: "warning" }, strings('ExperimentalTag'))));
        }
    }
    /** render the time the block started processing */
    timestamp() {
        if (!isEmpty(this.props.model) && (isProcessing(this.props.model) || isFinished(this.props.model))) {
            const replayedAndNotRerun = isReplay(this.props.model) && !hasBeenRerun(this.props.model) && !isBeingRerun(this.props.model);
            const completed = this.props.model.startTime && isWithCompleteEvent(this.props.model) && !isBeingRerun(this.props.model);
            const showingDate = !replayedAndNotRerun && completed && !this.props.isWidthConstrained;
            const status = isProcessing(this.props.model)
                ? 'processing'
                : isOk(this.props.model)
                    ? 'done'
                    : isOops(this.props.model)
                        ? 'error'
                        : 'not-yet';
            const { startTime } = this.props.model;
            const endTime = isWithCompleteEvent(this.props.model) &&
                !isBeingRerun(this.props.model) &&
                this.props.model.completeEvent.completeTime;
            return (React.createElement(Timer, { key: "timer", innerClassName: "small-left-pad sub-text", className: "kui--repl-block-timestamp kui--repl-block-right-element", status: status, endTime: endTime, startTime: startTime, showDate: showingDate }));
        }
    }
    /** spinner for processing blocks */
    spinner() {
        return React.createElement(Spinner, { key: "spinner" });
    }
    /** error icon for error blocks */
    /* private errorIcon() {
      if (isOops(this.props.model)) {
        return <Icons className="kui--repl-block-error-icon" icon="Error" data-mode="error" />
      }
    } */
    /** DropDown menu for completed blocks */
    actions(command) {
        if ((isFinished(this.props.model) || isProcessing(this.props.model)) && !!this.props.tab && !!this.props.model) {
            return React.createElement(Actions, Object.assign({ command: command, idx: this.props.idx }, this.props));
        }
    }
    /**
     * Status elements associated with the block as a whole; even though
     * these also pertain to the Output part of a Block, these are
     * currently housed in this Input component.
     *
     */
    status() {
        return React.createElement("span", { className: "repl-prompt-right-elements" });
    }
    /** Should we show the spinner in the In[...] context area, or in the [<input/> ...] area? */
    showSpinnerInContext() {
        return !this.props.isWidthConstrained;
    }
    /** Experimental tag, timestamp, etc. */
    rightElements() {
        const children = [
            this.experimentalTag(),
            !this.showSpinnerInContext() && isProcessing(this.props.model) && this.spinner(),
            this.timestamp()
        ].filter(_ => _);
        if (children.length > 0) {
            return React.createElement("span", { className: "repl-prompt-right-elements" }, children);
        }
    }
    /** Status elements placed in with <input> part of the block */
    inputStatus(input) {
        return (React.createElement(React.Fragment, null,
            this.rightElements(),
            this.actions(input)));
    }
}
//# sourceMappingURL=Input.js.map