// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { Dialog, showDialog } from '@jupyterlab/apputils';
import { CodeCellModel, MarkdownCellModel, RawCellModel } from '@jupyterlab/cells';
import * as nbformat from '@jupyterlab/nbformat';
import { ModelDB } from '@jupyterlab/observables';
import * as models from '@jupyter/ydoc';
import { nullTranslator } from '@jupyterlab/translation';
import { UUID } from '@lumino/coreutils';
import { Signal } from '@lumino/signaling';
import { CellList } from './celllist';
/**
 * An implementation of a notebook Model.
 */
export class NotebookModel {
    /**
     * Construct a new notebook model.
     */
    constructor(options = {}) {
        var _a;
        /**
         * A mutex to update the shared model.
         */
        this._modelDBMutex = models.createMutex();
        this._dirty = false;
        this._readOnly = false;
        this._contentChanged = new Signal(this);
        this._stateChanged = new Signal(this);
        this._nbformat = nbformat.MAJOR_VERSION;
        this._nbformatMinor = nbformat.MINOR_VERSION;
        this._isDisposed = false;
        if (options.modelDB) {
            this.modelDB = options.modelDB;
        }
        else {
            this.modelDB = new ModelDB();
        }
        this.sharedModel = new models.YNotebook({
            disableDocumentWideUndoRedo: (_a = options.disableDocumentWideUndoRedo) !== null && _a !== void 0 ? _a : false
        });
        this._isInitialized = options.isInitialized === false ? false : true;
        const factory = options.contentFactory || NotebookModel.defaultContentFactory;
        this.contentFactory = factory.clone(this.modelDB.view('cells'));
        this._cells = new CellList(this.modelDB, this.contentFactory, this.sharedModel);
        this._trans = (options.translator || nullTranslator).load('jupyterlab');
        this._cells.changed.connect(this._onCellsChanged, this);
        // Handle initial metadata.
        const metadata = this.modelDB.createMap('metadata');
        if (!metadata.has('language_info')) {
            const name = options.languagePreference || '';
            metadata.set('language_info', { name });
        }
        this._ensureMetadata();
        metadata.changed.connect(this._onMetadataChanged, this);
        this._deletedCells = [];
        this._collaborationEnabled = !!(options === null || options === void 0 ? void 0 : options.collaborationEnabled);
        this.sharedModel.changed.connect(this._onStateChanged, this);
    }
    /**
     * A signal emitted when the document content changes.
     */
    get contentChanged() {
        return this._contentChanged;
    }
    /**
     * A signal emitted when the document state changes.
     */
    get stateChanged() {
        return this._stateChanged;
    }
    /**
     * The dirty state of the document.
     */
    get dirty() {
        return this._dirty;
    }
    set dirty(newValue) {
        const oldValue = this._dirty;
        if (newValue === oldValue) {
            return;
        }
        this._dirty = newValue;
        this.triggerStateChange({
            name: 'dirty',
            oldValue,
            newValue
        });
    }
    /**
     * The read only state of the document.
     */
    get readOnly() {
        return this._readOnly;
    }
    set readOnly(newValue) {
        if (newValue === this._readOnly) {
            return;
        }
        const oldValue = this._readOnly;
        this._readOnly = newValue;
        this.triggerStateChange({ name: 'readOnly', oldValue, newValue });
    }
    /**
     * The metadata associated with the notebook.
     */
    get metadata() {
        return this.modelDB.get('metadata');
    }
    /**
     * Get the observable list of notebook cells.
     */
    get cells() {
        return this._cells;
    }
    /**
     * The major version number of the nbformat.
     */
    get nbformat() {
        return this._nbformat;
    }
    /**
     * The minor version number of the nbformat.
     */
    get nbformatMinor() {
        return this._nbformatMinor;
    }
    /**
     * The default kernel name of the document.
     */
    get defaultKernelName() {
        const spec = this.metadata.get('kernelspec');
        return spec ? spec.name : '';
    }
    /**
     * A list of deleted cells for the notebook..
     */
    get deletedCells() {
        return this._deletedCells;
    }
    /**
     * If the model is initialized or not.
     */
    get isInitialized() {
        return this._isInitialized;
    }
    /**
     * The default kernel language of the document.
     */
    get defaultKernelLanguage() {
        const info = this.metadata.get('language_info');
        return info ? info.name : '';
    }
    /**
     * Whether the model is collaborative or not.
     */
    get collaborative() {
        return this._collaborationEnabled;
    }
    /**
     * Dispose of the resources held by the model.
     */
    dispose() {
        // Do nothing if already disposed.
        if (this.isDisposed) {
            return;
        }
        const cells = this.cells;
        this._cells = null;
        cells.dispose();
        this._isDisposed = true;
        this.modelDB.dispose();
        Signal.clearData(this);
    }
    /**
     * Serialize the model to a string.
     */
    toString() {
        return JSON.stringify(this.toJSON());
    }
    /**
     * Deserialize the model from a string.
     *
     * #### Notes
     * Should emit a [contentChanged] signal.
     */
    fromString(value) {
        this.fromJSON(JSON.parse(value));
    }
    /**
     * Serialize the model to JSON.
     */
    toJSON() {
        var _a, _b;
        const cells = [];
        for (let i = 0; i < ((_b = (_a = this.cells) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0); i++) {
            const cell = this.cells.get(i).toJSON();
            if (this._nbformat === 4 && this._nbformatMinor <= 4) {
                // strip cell ids if we have notebook format 4.0-4.4
                delete cell.id;
            }
            cells.push(cell);
        }
        this._ensureMetadata();
        const metadata = this.sharedModel.getMetadata();
        for (const key of this.metadata.keys()) {
            metadata[key] = JSON.parse(JSON.stringify(this.metadata.get(key)));
        }
        return {
            metadata,
            nbformat_minor: this._nbformatMinor,
            nbformat: this._nbformat,
            cells
        };
    }
    /**
     * Deserialize the model from JSON.
     *
     * #### Notes
     * Should emit a [contentChanged] signal.
     */
    fromJSON(value) {
        const cells = [];
        const factory = this.contentFactory;
        const useId = value.nbformat === 4 && value.nbformat_minor >= 5;
        for (const cell of value.cells) {
            const options = { cell };
            if (useId) {
                options.id = cell.id;
            }
            switch (cell.cell_type) {
                case 'code':
                    cells.push(factory.createCodeCell(options));
                    break;
                case 'markdown':
                    cells.push(factory.createMarkdownCell(options));
                    break;
                case 'raw':
                    cells.push(factory.createRawCell(options));
                    break;
                default:
                    continue;
            }
        }
        if (!cells.length) {
            cells.push(factory.createCodeCell({}));
        }
        this.cells.beginCompoundOperation();
        this.cells.clear();
        this.cells.pushAll(cells);
        this.cells.endCompoundOperation();
        this.sharedModel.nbformat_minor =
            nbformat.MINOR_VERSION;
        this.sharedModel.nbformat = nbformat.MAJOR_VERSION;
        const origNbformat = value.metadata.orig_nbformat;
        if (value.nbformat !== this._nbformat) {
            this.sharedModel.nbformat = value.nbformat;
        }
        if (value.nbformat_minor > this._nbformatMinor) {
            this.sharedModel.nbformat_minor =
                value.nbformat_minor;
        }
        // Alert the user if the format changes.
        if (origNbformat !== undefined && this._nbformat !== origNbformat) {
            const newer = this._nbformat > origNbformat;
            let msg;
            if (newer) {
                msg = this._trans.__(`This notebook has been converted from an older notebook format (v%1)
to the current notebook format (v%2).
The next time you save this notebook, the current notebook format (v%2) will be used.
'Older versions of Jupyter may not be able to read the new format.' To preserve the original format version,
close the notebook without saving it.`, origNbformat, this._nbformat);
            }
            else {
                msg = this._trans.__(`This notebook has been converted from an newer notebook format (v%1)
to the current notebook format (v%2).
The next time you save this notebook, the current notebook format (v%2) will be used.
Some features of the original notebook may not be available.' To preserve the original format version,
close the notebook without saving it.`, origNbformat, this._nbformat);
            }
            void showDialog({
                title: this._trans.__('Notebook converted'),
                body: msg,
                buttons: [Dialog.okButton({ label: this._trans.__('Ok') })]
            });
        }
        // Update the metadata.
        this.metadata.clear();
        const metadata = value.metadata;
        for (const key in metadata) {
            // orig_nbformat is not intended to be stored per spec.
            if (key === 'orig_nbformat') {
                continue;
            }
            this.metadata.set(key, metadata[key]);
        }
        this._ensureMetadata();
        this.dirty = true;
    }
    /**
     * Initialize the model with its current state.
     *
     * # Notes
     * Adds an empty code cell if the model is empty
     * and clears undo state.
     */
    initialize() {
        if (!this.cells.length) {
            const factory = this.contentFactory;
            this.cells.push(factory.createCodeCell({}));
        }
        this._isInitialized = true;
        this.cells.clearUndo();
    }
    /**
     * Handle a change in the cells list.
     */
    _onCellsChanged(list, change) {
        switch (change.type) {
            case 'add':
                change.newValues.forEach(cell => {
                    cell.contentChanged.connect(this.triggerContentChange, this);
                });
                break;
            case 'remove':
                break;
            case 'set':
                change.newValues.forEach(cell => {
                    cell.contentChanged.connect(this.triggerContentChange, this);
                });
                break;
            default:
                break;
        }
        this.triggerContentChange();
    }
    _onStateChanged(sender, changes) {
        if (changes.stateChange) {
            changes.stateChange.forEach(value => {
                if (value.name === 'dirty') {
                    // Setting `dirty` will trigger the state change.
                    // We always set `dirty` because the shared model state
                    // and the local attribute are synchronized one way shared model -> _dirty
                    this.dirty = value.newValue;
                }
                else if (value.oldValue !== value.newValue) {
                    this.triggerStateChange(Object.assign({ newValue: undefined, oldValue: undefined }, value));
                }
            });
        }
        if (changes.nbformatChanged) {
            const change = changes.nbformatChanged;
            if (change.key === 'nbformat' && change.newValue !== undefined) {
                this._nbformat = change.newValue;
            }
            if (change.key === 'nbformat_minor' && change.newValue !== undefined) {
                this._nbformatMinor = change.newValue;
            }
        }
        if (changes.metadataChange) {
            const metadata = this.sharedModel.getMetadata();
            if (metadata) {
                this._modelDBMutex(() => {
                    this.metadata.clear();
                    Object.entries(metadata).forEach(([key, value]) => {
                        this.metadata.set(key, value);
                    });
                });
            }
        }
    }
    _onMetadataChanged(metadata, change) {
        this._modelDBMutex(() => {
            this.sharedModel.setMetadata(metadata.toJSON());
        });
        this.triggerContentChange();
    }
    /**
     * Make sure we have the required metadata fields.
     */
    _ensureMetadata() {
        const metadata = this.metadata;
        if (!metadata.has('language_info')) {
            metadata.set('language_info', { name: '' });
        }
        if (!metadata.has('kernelspec')) {
            metadata.set('kernelspec', { name: '', display_name: '' });
        }
    }
    /**
     * Trigger a state change signal.
     */
    triggerStateChange(args) {
        this._stateChanged.emit(args);
    }
    /**
     * Trigger a content changed signal.
     */
    triggerContentChange() {
        this._contentChanged.emit(void 0);
        this.dirty = true;
    }
    /**
     * Whether the model is disposed.
     */
    get isDisposed() {
        return this._isDisposed;
    }
}
/**
 * The namespace for the `NotebookModel` class statics.
 */
(function (NotebookModel) {
    /**
     * The default implementation of an `IContentFactory`.
     */
    class ContentFactory {
        /**
         * Create a new cell model factory.
         */
        constructor(options) {
            this.codeCellContentFactory =
                options.codeCellContentFactory || CodeCellModel.defaultContentFactory;
            this.modelDB = options.modelDB;
        }
        /**
         * Create a new cell by cell type.
         *
         * @param type:  the type of the cell to create.
         *
         * @param options: the cell creation options.
         *
         * #### Notes
         * This method is intended to be a convenience method to programmatically
         * call the other cell creation methods in the factory.
         */
        createCell(type, options) {
            switch (type) {
                case 'code':
                    return this.createCodeCell(options);
                case 'markdown':
                    return this.createMarkdownCell(options);
                case 'raw':
                default:
                    return this.createRawCell(options);
            }
        }
        /**
         * Create a new code cell.
         *
         * @param source - The data to use for the original source data.
         *
         * @returns A new code cell. If a source cell is provided, the
         *   new cell will be initialized with the data from the source.
         *   If the contentFactory is not provided, the instance
         *   `codeCellContentFactory` will be used.
         */
        createCodeCell(options) {
            if (options.contentFactory) {
                options.contentFactory = this.codeCellContentFactory;
            }
            if (this.modelDB) {
                if (!options.id) {
                    options.id = UUID.uuid4();
                }
                options.modelDB = this.modelDB.view(options.id);
            }
            return new CodeCellModel(options);
        }
        /**
         * Create a new markdown cell.
         *
         * @param source - The data to use for the original source data.
         *
         * @returns A new markdown cell. If a source cell is provided, the
         *   new cell will be initialized with the data from the source.
         */
        createMarkdownCell(options) {
            if (this.modelDB) {
                if (!options.id) {
                    options.id = UUID.uuid4();
                }
                options.modelDB = this.modelDB.view(options.id);
            }
            return new MarkdownCellModel(options);
        }
        /**
         * Create a new raw cell.
         *
         * @param source - The data to use for the original source data.
         *
         * @returns A new raw cell. If a source cell is provided, the
         *   new cell will be initialized with the data from the source.
         */
        createRawCell(options) {
            if (this.modelDB) {
                if (!options.id) {
                    options.id = UUID.uuid4();
                }
                options.modelDB = this.modelDB.view(options.id);
            }
            return new RawCellModel(options);
        }
        /**
         * Clone the content factory with a new IModelDB.
         */
        clone(modelDB) {
            return new ContentFactory({
                modelDB: modelDB,
                codeCellContentFactory: this.codeCellContentFactory
            });
        }
    }
    NotebookModel.ContentFactory = ContentFactory;
    /**
     * The default `ContentFactory` instance.
     */
    NotebookModel.defaultContentFactory = new ContentFactory({});
})(NotebookModel || (NotebookModel = {}));
//# sourceMappingURL=model.js.map