/*
 * 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.
 */
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 React from 'react';
import { extname } from 'path';
import { editor as Monaco, Range } from 'monaco-editor';
import { i18n } from '@kui-shell/core/mdist/api/i18n';
import { encodeComponent } from '@kui-shell/core/mdist/api/Exec';
import { eventBus, eventChannelUnsafe } from '@kui-shell/core/mdist/api/Events';
import Icons from '../../spi/Icons';
import ClearButton from './ClearButton';
import SaveFileButton from './SaveFileButton';
import RevertFileButton from './RevertFileButton';
import getKuiFontSize from './lib/fonts';
import { language } from './lib/file-types';
import defaultMonacoOptions from './lib/defaults';
import '../../../../web/scss/components/Editor/Editor.scss';
const strings = i18n('plugin-client-common', 'editor');
export function isFile(file) {
    return file.apiVersion === 'kui-shell/v1' && file.kind === 'File';
}
export default class Editor extends React.PureComponent {
    constructor(props) {
        super(props);
        // created below in render() via ref={...} -> initMonaco()
        this.state = {
            content: props.content,
            cleaners: [],
            editor: undefined,
            wrapper: undefined,
            catastrophicError: undefined
        };
    }
    static getDerivedStateFromError(error) {
        return { catastrophicError: error };
    }
    componentDidCatch(error, errorInfo) {
        console.error('catastrophic error in Editor', error, errorInfo);
    }
    /**
     * ToolbarText for a clean editor, i.e. no changes have been made
     * since last save.
     *
     */
    static allClean(readOnly) {
        return {
            type: 'info',
            text: strings(!readOnly ? 'isUpToDate' : 'isUpToDateReadonly')
        };
    }
    /**
     * ToolbarText to indicate error saving.
     *
     */
    static error(props, msg, ...args) {
        return {
            type: 'error',
            text: strings(msg, ...args)
        };
    }
    /** Called whenever we have proposed (props,state); we derive a new State */
    static getDerivedStateFromProps(props, state) {
        if (!state.editor && state.wrapper) {
            // then we are ready to render monaco into the wrapper
            return Editor.initMonaco(props, state);
        }
        else if (!state.content ||
            props.content.content !== state.content.content ||
            props.content.contentType !== state.content.contentType) {
            return {
                content: props.content,
                subscription: Editor.reinitMonaco(props, state, Editor.isReadOnly(props, state))
            };
        }
        else {
            return state;
        }
    }
    /** Called when this component is no longer attached to the document */
    componentWillUnmount() {
        this.destroyMonaco();
    }
    /** Called when we no longer need the monaco-editor instance */
    destroyMonaco() {
        this.state.cleaners.forEach(cleaner => cleaner());
    }
    static isClearable(props, content) {
        return ((isFile(props.response) && !props.readOnly) ||
            (!isFile(props.response) && content.spec && content.spec.clearable !== false));
    }
    static onChange(props, content, readOnly, editor) {
        let currentDecorations;
        return (evt) => {
            // See initMonaco(): note how we first set `value: ''`, then we
            // asynchronously update the text, which results in an onChange
            // callback. Thus, we can safely ignore the first change, which
            // corresponds to a versionId of 2, since we first set `value:
            // ''`. https://github.com/kubernetes-sigs/kui/issues/7426
            if (evt.versionId === 2) {
                return;
            }
            if (currentDecorations) {
                editor.deltaDecorations(currentDecorations, []);
                currentDecorations = undefined;
            }
            const clearable = Editor.isClearable(props, content);
            const buttons = [];
            // save
            if (isFile(props.response)) {
                const save = SaveFileButton(editor, props.repl, props.response, (success) => {
                    if (success) {
                        props.willUpdateToolbar(Editor.allClean(readOnly), !clearable ? undefined : [ClearButton(editor)]);
                    }
                    else {
                        props.willUpdateToolbar(Editor.error(props, 'errorSaving'));
                    }
                });
                buttons.push(save);
            }
            else if (content.spec && content.spec.save) {
                const { onSave } = content.spec.save;
                buttons.push({
                    mode: 'Save',
                    label: content.spec.save.label || strings('saveLocalFile'),
                    kind: 'drilldown',
                    icon: React.createElement(Icons, { icon: "Save" }),
                    inPlace: true,
                    command: () => __awaiter(this, void 0, void 0, function* () {
                        try {
                            const save = yield onSave(editor.getValue());
                            if (!(save && save.noToolbarUpdate)) {
                                props.willUpdateToolbar((save && save.toolbarText) || Editor.allClean(readOnly), !clearable ? undefined : [ClearButton(editor)]);
                            }
                            /** return the command to be executed */
                            if (save && save.command) {
                                return save.command;
                            }
                        }
                        catch (error) {
                            const err = error;
                            if (err.revealLine) {
                                editor.revealLineInCenter(err.revealLine);
                                currentDecorations = editor.deltaDecorations(currentDecorations || [], [
                                    {
                                        range: new Range(err.revealLine, 1, err.revealLine, 1),
                                        options: {
                                            isWholeLine: true,
                                            className: 'kui--editor-line-highlight',
                                            glyphMarginClassName: 'kui--editor-line-glyph'
                                        }
                                    }
                                ]);
                            }
                            const alert = {
                                type: 'warning',
                                text: strings('isModified'),
                                alerts: [
                                    {
                                        type: 'error',
                                        title: strings('errorApplying'),
                                        body: err.message
                                    }
                                ]
                            };
                            props.willUpdateToolbar(alert, buttons, undefined);
                        }
                    })
                });
            }
            // revert
            if (isFile(props.response)) {
                const revert = RevertFileButton(editor, props.repl, props.response, (success, data) => {
                    if (success) {
                        editor.setValue(data);
                        props.willUpdateToolbar(Editor.allClean(readOnly), !clearable ? undefined : [ClearButton(editor)]);
                    }
                    else {
                        props.willUpdateToolbar(Editor.error(props, 'errorReverting'));
                    }
                });
                buttons.push(revert);
            }
            else if (content.spec && content.spec.revert) {
                const { onRevert } = content.spec.revert;
                buttons.push({
                    mode: 'Revert',
                    label: content.spec.revert.label || strings('revert'),
                    kind: 'view',
                    icon: React.createElement(Icons, { icon: "Revert" }),
                    command: () => __awaiter(this, void 0, void 0, function* () {
                        try {
                            const data = yield onRevert();
                            editor.setValue(data);
                            props.willUpdateToolbar(Editor.allClean(readOnly), !clearable ? undefined : [ClearButton(editor)]);
                        }
                        catch (err) {
                            console.error(err);
                            props.willUpdateToolbar(Editor.error(props, 'errorReverting'));
                        }
                    })
                });
            }
            // clear
            if (clearable) {
                buttons.push(ClearButton(editor));
            }
            props.willUpdateToolbar({
                type: 'warning',
                text: strings('isModified')
            }, buttons);
        };
    }
    /** Handle Toolbar registrations */
    static subscribeToChanges(props, content, editor, readOnly) {
        if (props.willUpdateToolbar) {
            // send an initial update; note how the initial toolbarText may
            // be governed by the response
            const msg = (readOnly && props.response.toolbarText) || (!readOnly && Editor.allClean(readOnly)); // <-- always use allClean if !readOnly
            const buttons = props.response.toolbarText
                ? []
                : !Editor.isClearable(props, content)
                    ? undefined
                    : [ClearButton(editor)];
            props.willUpdateToolbar(msg, buttons);
            // then subscribe to future model change events
            return editor.onDidChangeModelContent(Editor.onChange(props, content, readOnly, editor));
        }
    }
    static extractValue(content, response, repl) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                return (typeof content.content !== 'string' || content.content.length === 0) && isFile(response)
                    ? yield repl.qexec(`vfs fslice ${encodeComponent(response.spec.fullpath)} ${0} ${8192}`)
                    : content.content;
            }
            catch (err) {
                console.error(err);
                return err.message;
            }
        });
    }
    static reinitMonaco(props, state, readOnly) {
        setTimeout(() => __awaiter(this, void 0, void 0, function* () {
            state.editor.setValue(yield Editor.extractValue(props.content, props.response, props.repl));
            const msg = (readOnly && props.response.toolbarText) || (!readOnly && Editor.allClean(readOnly)); // <-- always use allClean if !readOnly
            props.willUpdateToolbar(msg, !Editor.isClearable(props, props.content) ? undefined : [ClearButton(state.editor)]);
            if (state.subscription) {
                state.subscription.dispose();
            }
        }));
        return Editor.subscribeToChanges(props, props.content, state.editor, Editor.isReadOnly(props, state));
    }
    static isReadOnly(props, state) {
        return (!isFile(props.response) &&
            (!state.content.spec || state.content.spec.readOnly !== false) &&
            (props.readOnly || !isFile(props.response) || false));
    }
    /** Called when we have a ready wrapper (monaco's init requires an wrapper */
    static initMonaco(props, state) {
        const cleaners = [];
        try {
            // here we instantiate an editor widget
            const providedOptions = {
                value: '',
                showLineNumbers: true,
                readOnly: Editor.isReadOnly(props, state),
                language: state.content.contentType === 'text/plain'
                    ? language(state.content.contentType, isFile(props.response) ? extname(props.response.spec.filepath).slice(1) : undefined)
                    : state.content.contentType || undefined
            };
            const overrides = { theme: props.light ? 'vs' : 'vs-dark' };
            const options = Object.assign(defaultMonacoOptions(providedOptions), providedOptions, overrides);
            const editor = Monaco.create(state.wrapper, options);
            setTimeout(() => __awaiter(this, void 0, void 0, function* () {
                editor.setValue(yield Editor.extractValue(state.content, props.response, props.repl));
                // initial default folding level; see https://github.com/kubernetes-sigs/kui/issues/8008
                editor.getAction('editor.foldLevel3').run();
            }));
            const onZoom = () => {
                editor.updateOptions({ fontSize: getKuiFontSize() });
            };
            eventChannelUnsafe.on('/zoom', onZoom);
            cleaners.push(() => eventChannelUnsafe.off('/zoom', onZoom));
            const sizeToFit = !props.sizeToFit
                ? () => true
                : (width, height = Math.min(0.4 * window.innerHeight, Math.max(250, editor.getContentHeight()))) => {
                    // if we know 1) the height of the content won't change, and
                    // 2) we are running in "simple" mode (this is mostly the case
                    // for inline editor components, as opposed to editor
                    // components that are intended to fill the full view), then:
                    // size the height to fit the content
                    state.wrapper.style.flexBasis = height + 'px';
                };
            sizeToFit();
            const observer = new ResizeObserver(entries => {
                sizeToFit(entries[0].contentRect.width, entries[0].contentRect.height);
                editor.layout();
            });
            observer.observe(state.wrapper);
            cleaners.push(() => observer.disconnect());
            const onTabLayoutChange = () => {
                sizeToFit();
                editor.layout();
            };
            eventBus.onTabLayoutChange(props.tabUUID, onTabLayoutChange);
            cleaners.push(() => eventBus.offTabLayoutChange(props.tabUUID, onTabLayoutChange));
            editor['clearDecorations'] = () => {
                // debug('clearing decorations', editor['__cloudshell_decorations'])
                const none = [{ range: new Range(1, 1, 1, 1), options: {} }];
                editor['__cloudshell_decorations'] = editor.deltaDecorations(editor['__cloudshell_decorations'] || [], none);
            };
            state.wrapper['getValueForTests'] = () => {
                return editor.getValue();
            };
            if (!options.readOnly) {
                setTimeout(() => editor.focus());
            }
            const subscription = Editor.subscribeToChanges(props, props.content, editor, options.readOnly);
            if (subscription) {
                cleaners.push(() => subscription.dispose());
            }
            cleaners.push(() => {
                editor.dispose();
                const model = editor.getModel();
                if (model) {
                    model.dispose();
                }
            });
            return {
                editor,
                cleaners,
                subscription
            };
        }
        catch (err) {
            console.error('Error initing Monaco: ', err);
            state.catastrophicError = err;
            return state;
        }
    }
    render() {
        if (this.state.catastrophicError) {
            return React.createElement("div", { className: "oops" },
                " ",
                this.state.catastrophicError.toString());
        }
        else {
            return (React.createElement("div", { className: "code-highlighting" },
                React.createElement("div", { className: "monaco-editor-wrapper", ref: wrapper => this.setState({ wrapper }) })));
        }
    }
}
//# sourceMappingURL=index.js.map