// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { Collapse, Styling } from '@jupyterlab/apputils';
import { CodeEditor, CodeEditorWrapper, JSONEditor } from '@jupyterlab/codeeditor';
import { ObservableJSON } from '@jupyterlab/observables';
import { nullTranslator } from '@jupyterlab/translation';
import { ArrayExt, chain, each } from '@lumino/algorithm';
import { ConflatableMessage, MessageLoop } from '@lumino/messaging';
import { h, VirtualDOM } from '@lumino/virtualdom';
import { PanelLayout, Widget } from '@lumino/widgets';
class RankedPanel extends Widget {
    constructor() {
        super();
        this._items = [];
        this.layout = new PanelLayout();
        this.addClass('jp-RankedPanel');
    }
    addWidget(widget, rank) {
        const rankItem = { widget, rank };
        const index = ArrayExt.upperBound(this._items, rankItem, Private.itemCmp);
        ArrayExt.insert(this._items, index, rankItem);
        const layout = this.layout;
        layout.insertWidget(index, widget);
    }
    /**
     * Handle the removal of a child
     *
     */
    onChildRemoved(msg) {
        const index = ArrayExt.findFirstIndex(this._items, item => item.widget === msg.child);
        if (index !== -1) {
            ArrayExt.removeAt(this._items, index);
        }
    }
}
/**
 * A widget that provides metadata tools.
 */
export class NotebookTools extends Widget {
    /**
     * Construct a new NotebookTools object.
     */
    constructor(options) {
        super();
        this.addClass('jp-NotebookTools');
        this.translator = options.translator || nullTranslator;
        this._trans = this.translator.load('jupyterlab');
        this._commonTools = new RankedPanel();
        this._advancedTools = new RankedPanel();
        this._advancedTools.title.label = this._trans.__('Advanced Tools');
        const layout = (this.layout = new PanelLayout());
        layout.addWidget(this._commonTools);
        layout.addWidget(new Collapse({ widget: this._advancedTools }));
        this._tracker = options.tracker;
        this._tracker.currentChanged.connect(this._onActiveNotebookPanelChanged, this);
        this._tracker.activeCellChanged.connect(this._onActiveCellChanged, this);
        this._tracker.selectionChanged.connect(this._onSelectionChanged, this);
        this._onActiveNotebookPanelChanged();
        this._onActiveCellChanged();
        this._onSelectionChanged();
    }
    /**
     * The active cell widget.
     */
    get activeCell() {
        return this._tracker.activeCell;
    }
    /**
     * The currently selected cells.
     */
    get selectedCells() {
        const panel = this._tracker.currentWidget;
        if (!panel) {
            return [];
        }
        const notebook = panel.content;
        return notebook.widgets.filter(cell => notebook.isSelectedOrActive(cell));
    }
    /**
     * The current notebook.
     */
    get activeNotebookPanel() {
        return this._tracker.currentWidget;
    }
    /**
     * Add a cell tool item.
     */
    addItem(options) {
        var _a;
        const tool = options.tool;
        const rank = (_a = options.rank) !== null && _a !== void 0 ? _a : 100;
        let section;
        if (options.section === 'advanced') {
            section = this._advancedTools;
        }
        else {
            section = this._commonTools;
        }
        tool.addClass('jp-NotebookTools-tool');
        section.addWidget(tool, rank);
        // TODO: perhaps the necessary notebookTools functionality should be
        // consolidated into a single object, rather than a broad reference to this.
        tool.notebookTools = this;
        // Trigger the tool to update its active notebook and cell.
        MessageLoop.sendMessage(tool, NotebookTools.ActiveNotebookPanelMessage);
        MessageLoop.sendMessage(tool, NotebookTools.ActiveCellMessage);
    }
    /**
     * Handle a change to the notebook panel.
     */
    _onActiveNotebookPanelChanged() {
        if (this._prevActiveNotebookModel &&
            !this._prevActiveNotebookModel.isDisposed) {
            this._prevActiveNotebookModel.metadata.changed.disconnect(this._onActiveNotebookPanelMetadataChanged, this);
        }
        const activeNBModel = this.activeNotebookPanel && this.activeNotebookPanel.content
            ? this.activeNotebookPanel.content.model
            : null;
        this._prevActiveNotebookModel = activeNBModel;
        if (activeNBModel) {
            activeNBModel.metadata.changed.connect(this._onActiveNotebookPanelMetadataChanged, this);
        }
        each(this._toolChildren(), widget => {
            MessageLoop.sendMessage(widget, NotebookTools.ActiveNotebookPanelMessage);
        });
    }
    /**
     * Handle a change to the active cell.
     */
    _onActiveCellChanged() {
        if (this._prevActiveCell && !this._prevActiveCell.isDisposed) {
            this._prevActiveCell.metadata.changed.disconnect(this._onActiveCellMetadataChanged, this);
        }
        const activeCell = this.activeCell ? this.activeCell.model : null;
        this._prevActiveCell = activeCell;
        if (activeCell) {
            activeCell.metadata.changed.connect(this._onActiveCellMetadataChanged, this);
        }
        each(this._toolChildren(), widget => {
            MessageLoop.sendMessage(widget, NotebookTools.ActiveCellMessage);
        });
    }
    /**
     * Handle a change in the selection.
     */
    _onSelectionChanged() {
        each(this._toolChildren(), widget => {
            MessageLoop.sendMessage(widget, NotebookTools.SelectionMessage);
        });
    }
    /**
     * Handle a change in the active cell metadata.
     */
    _onActiveNotebookPanelMetadataChanged(sender, args) {
        const message = new ObservableJSON.ChangeMessage('activenotebookpanel-metadata-changed', args);
        each(this._toolChildren(), widget => {
            MessageLoop.sendMessage(widget, message);
        });
    }
    /**
     * Handle a change in the notebook model metadata.
     */
    _onActiveCellMetadataChanged(sender, args) {
        const message = new ObservableJSON.ChangeMessage('activecell-metadata-changed', args);
        each(this._toolChildren(), widget => {
            MessageLoop.sendMessage(widget, message);
        });
    }
    _toolChildren() {
        return chain(this._commonTools.children(), this._advancedTools.children());
    }
}
/**
 * The namespace for NotebookTools class statics.
 */
(function (NotebookTools) {
    /**
     * A singleton conflatable `'activenotebookpanel-changed'` message.
     */
    NotebookTools.ActiveNotebookPanelMessage = new ConflatableMessage('activenotebookpanel-changed');
    /**
     * A singleton conflatable `'activecell-changed'` message.
     */
    NotebookTools.ActiveCellMessage = new ConflatableMessage('activecell-changed');
    /**
     * A singleton conflatable `'selection-changed'` message.
     */
    NotebookTools.SelectionMessage = new ConflatableMessage('selection-changed');
    /**
     * The base notebook tool, meant to be subclassed.
     */
    class Tool extends Widget {
        dispose() {
            super.dispose();
            if (this.notebookTools) {
                this.notebookTools = null;
            }
        }
        /**
         * Process a message sent to the widget.
         *
         * @param msg - The message sent to the widget.
         */
        processMessage(msg) {
            super.processMessage(msg);
            switch (msg.type) {
                case 'activenotebookpanel-changed':
                    this.onActiveNotebookPanelChanged(msg);
                    break;
                case 'activecell-changed':
                    this.onActiveCellChanged(msg);
                    break;
                case 'selection-changed':
                    this.onSelectionChanged(msg);
                    break;
                case 'activecell-metadata-changed':
                    this.onActiveCellMetadataChanged(msg);
                    break;
                case 'activenotebookpanel-metadata-changed':
                    this.onActiveNotebookPanelMetadataChanged(msg);
                    break;
                default:
                    break;
            }
        }
        /**
         * Handle a change to the notebook panel.
         *
         * #### Notes
         * The default implementation is a no-op.
         */
        onActiveNotebookPanelChanged(msg) {
            /* no-op */
        }
        /**
         * Handle a change to the active cell.
         *
         * #### Notes
         * The default implementation is a no-op.
         */
        onActiveCellChanged(msg) {
            /* no-op */
        }
        /**
         * Handle a change to the selection.
         *
         * #### Notes
         * The default implementation is a no-op.
         */
        onSelectionChanged(msg) {
            /* no-op */
        }
        /**
         * Handle a change to the metadata of the active cell.
         *
         * #### Notes
         * The default implementation is a no-op.
         */
        onActiveCellMetadataChanged(msg) {
            /* no-op */
        }
        /**
         * Handle a change to the metadata of the active cell.
         *
         * #### Notes
         * The default implementation is a no-op.
         */
        onActiveNotebookPanelMetadataChanged(msg) {
            /* no-op */
        }
    }
    NotebookTools.Tool = Tool;
    /**
     * A cell tool displaying the active cell contents.
     */
    class ActiveCellTool extends Tool {
        /**
         * Construct a new active cell tool.
         */
        constructor() {
            super();
            this._model = new CodeEditor.Model();
            this.addClass('jp-ActiveCellTool');
            this.addClass('jp-InputArea');
            this.layout = new PanelLayout();
        }
        /**
         * Dispose of the resources used by the tool.
         */
        dispose() {
            if (this._model === null) {
                return;
            }
            this._model.dispose();
            this._model = null;
            super.dispose();
        }
        /**
         * Handle a change to the active cell.
         */
        onActiveCellChanged() {
            const activeCell = this.notebookTools.activeCell;
            const layout = this.layout;
            const count = layout.widgets.length;
            for (let i = 0; i < count; i++) {
                layout.widgets[0].dispose();
            }
            if (this._cellModel && !this._cellModel.isDisposed) {
                this._cellModel.value.changed.disconnect(this._onValueChanged, this);
                this._cellModel.mimeTypeChanged.disconnect(this._onMimeTypeChanged, this);
            }
            if (!activeCell) {
                const cell = new Widget();
                cell.addClass('jp-InputArea-editor');
                cell.addClass('jp-InputArea-editor');
                layout.addWidget(cell);
                this._cellModel = null;
                return;
            }
            const promptNode = activeCell.promptNode
                ? activeCell.promptNode.cloneNode(true)
                : undefined;
            const prompt = new Widget({ node: promptNode });
            const factory = activeCell.contentFactory.editorFactory;
            const cellModel = (this._cellModel = activeCell.model);
            cellModel.value.changed.connect(this._onValueChanged, this);
            cellModel.mimeTypeChanged.connect(this._onMimeTypeChanged, this);
            this._model.value.text = cellModel.value.text.split('\n')[0];
            this._model.mimeType = cellModel.mimeType;
            const model = this._model;
            const editorWidget = new CodeEditorWrapper({ model, factory });
            editorWidget.addClass('jp-InputArea-editor');
            editorWidget.addClass('jp-InputArea-editor');
            editorWidget.editor.setOption('readOnly', true);
            layout.addWidget(prompt);
            layout.addWidget(editorWidget);
        }
        /**
         * Handle a change to the notebook panel.
         *
         * #### Notes
         * The default implementation is a no-op.
         */
        onActiveNotebookPanelChanged(msg) {
            if (!this.notebookTools.activeNotebookPanel) {
                // Force cleaning up the signal
                void this.onActiveCellChanged();
            }
        }
        /**
         * Handle a change to the current editor value.
         */
        _onValueChanged() {
            this._model.value.text = this._cellModel.value.text.split('\n')[0];
        }
        /**
         * Handle a change to the current editor mimetype.
         */
        _onMimeTypeChanged() {
            this._model.mimeType = this._cellModel.mimeType;
        }
    }
    NotebookTools.ActiveCellTool = ActiveCellTool;
    /**
     * A raw metadata editor.
     */
    class MetadataEditorTool extends Tool {
        /**
         * Construct a new raw metadata tool.
         */
        constructor(options) {
            super();
            const { editorFactory } = options;
            this.addClass('jp-MetadataEditorTool');
            const layout = (this.layout = new PanelLayout());
            this._editorFactory = editorFactory;
            this._editorLabel = options.label || 'Edit Metadata';
            this.createEditor();
            const titleNode = new Widget({ node: document.createElement('label') });
            titleNode.node.textContent = options.label || 'Edit Metadata';
            layout.addWidget(titleNode);
            layout.addWidget(this.editor);
        }
        /**
         * The editor used by the tool.
         */
        get editor() {
            return this._editor;
        }
        /**
         * Handle a change to the notebook.
         */
        onActiveNotebookPanelChanged(msg) {
            this.editor.dispose();
            if (this.notebookTools.activeNotebookPanel) {
                this.createEditor();
            }
        }
        createEditor() {
            this._editor = new JSONEditor({
                editorFactory: this._editorFactory
            });
            this.editor.title.label = this._editorLabel;
            this.layout.addWidget(this.editor);
        }
    }
    NotebookTools.MetadataEditorTool = MetadataEditorTool;
    /**
     * A notebook metadata editor
     */
    class NotebookMetadataEditorTool extends MetadataEditorTool {
        constructor(options) {
            const translator = options.translator || nullTranslator;
            const trans = translator.load('jupyterlab');
            options.label = options.label || trans.__('Notebook Metadata');
            super(options);
        }
        /**
         * Handle a change to the notebook.
         */
        onActiveNotebookPanelChanged(msg) {
            super.onActiveNotebookPanelChanged(msg);
            if (this.notebookTools.activeNotebookPanel) {
                this._update();
            }
        }
        /**
         * Handle a change to the notebook metadata.
         */
        onActiveNotebookPanelMetadataChanged(msg) {
            this._update();
        }
        _update() {
            var _a, _b;
            const nb = this.notebookTools.activeNotebookPanel &&
                this.notebookTools.activeNotebookPanel.content;
            this.editor.source = (_b = (_a = nb === null || nb === void 0 ? void 0 : nb.model) === null || _a === void 0 ? void 0 : _a.metadata) !== null && _b !== void 0 ? _b : null;
        }
    }
    NotebookTools.NotebookMetadataEditorTool = NotebookMetadataEditorTool;
    /**
     * A cell metadata editor
     */
    class CellMetadataEditorTool extends MetadataEditorTool {
        constructor(options) {
            const translator = options.translator || nullTranslator;
            const trans = translator.load('jupyterlab');
            options.label = options.label || trans.__('Cell Metadata');
            super(options);
        }
        /**
         * Handle a change to the active cell.
         */
        onActiveCellChanged(msg) {
            this.editor.dispose();
            if (this.notebookTools.activeCell) {
                this.createEditor();
                this._update();
            }
        }
        /**
         * Handle a change to the active cell metadata.
         */
        onActiveCellMetadataChanged(msg) {
            this._update();
        }
        _update() {
            const cell = this.notebookTools.activeCell;
            this.editor.source = cell ? cell.model.metadata : null;
        }
    }
    NotebookTools.CellMetadataEditorTool = CellMetadataEditorTool;
    /**
     * A cell tool that provides a selection for a given metadata key.
     */
    class KeySelector extends Tool {
        /**
         * Construct a new KeySelector.
         */
        constructor(options) {
            // TODO: use react
            super({ node: Private.createSelectorNode(options) });
            /**
             * Get the value for the data.
             */
            this._getValue = (cell) => {
                let value = cell.model.metadata.get(this.key);
                if (value === undefined) {
                    value = this._default;
                }
                return value;
            };
            /**
             * Set the value for the data.
             */
            this._setValue = (cell, value) => {
                if (value === this._default) {
                    cell.model.metadata.delete(this.key);
                }
                else {
                    cell.model.metadata.set(this.key, value);
                }
            };
            this._changeGuard = false;
            this.addClass('jp-KeySelector');
            this.key = options.key;
            this._default = options.default;
            this._validCellTypes = options.validCellTypes || [];
            this._getter = options.getter || this._getValue;
            this._setter = options.setter || this._setValue;
        }
        /**
         * The select node for the widget.
         */
        get selectNode() {
            return this.node.getElementsByTagName('select')[0];
        }
        /**
         * Handle the DOM events for the widget.
         *
         * @param event - The DOM event sent to the widget.
         *
         * #### Notes
         * This method implements the DOM `EventListener` interface and is
         * called in response to events on the notebook panel's node. It should
         * not be called directly by user code.
         */
        handleEvent(event) {
            switch (event.type) {
                case 'change':
                    this.onValueChanged();
                    break;
                default:
                    break;
            }
        }
        /**
         * Handle `after-attach` messages for the widget.
         */
        onAfterAttach(msg) {
            const node = this.selectNode;
            node.addEventListener('change', this);
        }
        /**
         * Handle `before-detach` messages for the widget.
         */
        onBeforeDetach(msg) {
            const node = this.selectNode;
            node.removeEventListener('change', this);
        }
        /**
         * Handle a change to the active cell.
         */
        onActiveCellChanged(msg) {
            const select = this.selectNode;
            const activeCell = this.notebookTools.activeCell;
            if (!activeCell) {
                select.disabled = true;
                select.value = '';
                return;
            }
            const cellType = activeCell.model.type;
            if (this._validCellTypes.length &&
                this._validCellTypes.indexOf(cellType) === -1) {
                select.value = '';
                select.disabled = true;
                return;
            }
            select.disabled = false;
            this._changeGuard = true;
            const getter = this._getter;
            select.value = JSON.stringify(getter(activeCell));
            this._changeGuard = false;
        }
        /**
         * Handle a change to the metadata of the active cell.
         */
        onActiveCellMetadataChanged(msg) {
            if (this._changeGuard) {
                return;
            }
            const select = this.selectNode;
            const cell = this.notebookTools.activeCell;
            if (msg.args.key === this.key && cell) {
                this._changeGuard = true;
                const getter = this._getter;
                select.value = JSON.stringify(getter(cell));
                this._changeGuard = false;
            }
        }
        /**
         * Handle a change to the value.
         */
        onValueChanged() {
            const activeCell = this.notebookTools.activeCell;
            if (!activeCell || this._changeGuard) {
                return;
            }
            this._changeGuard = true;
            const select = this.selectNode;
            const setter = this._setter;
            setter(activeCell, JSON.parse(select.value));
            this._changeGuard = false;
        }
    }
    NotebookTools.KeySelector = KeySelector;
    /**
     * Create a slideshow selector.
     */
    function createSlideShowSelector(translator) {
        translator = translator || nullTranslator;
        const trans = translator.load('jupyterlab');
        trans.__('');
        const options = {
            key: 'slideshow',
            title: trans.__('Slide Type'),
            optionValueArray: [
                ['-', null],
                [trans.__('Slide'), 'slide'],
                [trans.__('Sub-Slide'), 'subslide'],
                [trans.__('Fragment'), 'fragment'],
                [trans.__('Skip'), 'skip'],
                [trans.__('Notes'), 'notes']
            ],
            getter: cell => {
                const value = cell.model.metadata.get('slideshow');
                return value && value['slide_type'];
            },
            setter: (cell, value) => {
                let data = cell.model.metadata.get('slideshow') || Object.create(null);
                if (value === null) {
                    // Make a shallow copy so we aren't modifying the original metadata.
                    data = Object.assign({}, data);
                    delete data.slide_type;
                }
                else {
                    data = Object.assign(Object.assign({}, data), { slide_type: value });
                }
                if (Object.keys(data).length > 0) {
                    cell.model.metadata.set('slideshow', data);
                }
                else {
                    cell.model.metadata.delete('slideshow');
                }
            }
        };
        return new KeySelector(options);
    }
    NotebookTools.createSlideShowSelector = createSlideShowSelector;
    /**
     * Create an nbconvert selector.
     */
    function createNBConvertSelector(optionValueArray, translator) {
        translator = translator || nullTranslator;
        const trans = translator.load('jupyterlab');
        return new KeySelector({
            key: 'raw_mimetype',
            title: trans.__('Raw NBConvert Format'),
            optionValueArray: optionValueArray,
            validCellTypes: ['raw']
        });
    }
    NotebookTools.createNBConvertSelector = createNBConvertSelector;
})(NotebookTools || (NotebookTools = {}));
/**
 * A namespace for private data.
 */
var Private;
(function (Private) {
    /**
     * A comparator function for widget rank items.
     */
    function itemCmp(first, second) {
        return first.rank - second.rank;
    }
    Private.itemCmp = itemCmp;
    /**
     * Create the node for a KeySelector.
     */
    function createSelectorNode(options) {
        const name = options.key;
        const title = options.title || name[0].toLocaleUpperCase() + name.slice(1);
        const optionNodes = [];
        let value;
        let option;
        each(options.optionValueArray, item => {
            option = item[0];
            value = JSON.stringify(item[1]);
            optionNodes.push(h.option({ value }, option));
        });
        const node = VirtualDOM.realize(h.div({}, h.label(title, h.select({}, optionNodes))));
        Styling.styleNode(node);
        return node;
    }
    Private.createSelectorNode = createSelectorNode;
})(Private || (Private = {}));
//# sourceMappingURL=notebooktools.js.map