/*
* 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.
**/

/**
 * @namespace RED.editor.codeEditor.monaco
*/

/*
    The code editor currenlty supports 2 functions init and create.
    * Init() - setup the editor / must return true
    * Create() - create an editor instance / returns an editor as generated by the editor lib
    * To be compatable with the original ace lib (for contrib nodes using it), the object returned by create() must (at minimum) support the following...
        property .selection = {};
        function .selection.getRange();
        property .session //the editor object
        function .session.insert = function(position, text)
        function .setReadOnly(readOnly)
        property .renderer = {};
        function .renderer.updateFull()
        function setMode(mode, cb)
        function getRange();
        function replace(range, text)
        function selectAll
        function clearSelection
        function getSelectedText()
        function destroy()
        function resize()
        function getSession()
        function getLength()
        function scrollToLine(lineNumber, scrollType)
        function moveCursorTo(lineNumber, colNumber)
        function getAnnotations()
        function gotoLine(row, col)
        function getCursorPosition()
        function setTheme(name)
        function setFontSize(size:Number) //Set a new font size (in pixels) for the editor text.
        function on(name, cb)
        function getUndoManager()

*/

RED.editor.codeEditor.monaco = (function() {
    var initialised = false;
    const type = "monaco";
    const monacoThemes = ["vs","vs-dark","hc-black"]; //TODO: consider setting hc-black autmatically based on acessability?
    let userSelectedTheme;

    //TODO: get from externalModules.js  For now this is enough for feature parity with ACE (and then some).
    const knownModules = {
        "assert": {package: "node", module: "assert", path: "node/assert.d.ts" },
        "assert/strict": {package: "node", module: "assert/strict", path: "node/assert/strict.d.ts" },
        "async_hooks": {package: "node", module: "async_hooks", path: "node/async_hooks.d.ts" },
        "buffer": {package: "node", module: "buffer", path: "node/buffer.d.ts" },
        "child_process": {package: "node", module: "child_process", path: "node/child_process.d.ts" },
        "cluster": {package: "node", module: "cluster", path: "node/cluster.d.ts" },
        "console": {package: "node", module: "console", path: "node/console.d.ts" },
        "crypto": {package: "node", module: "crypto", path: "node/crypto.d.ts" },
        "dgram": {package: "node", module: "dgram", path: "node/dgram.d.ts" },
        "diagnostics_channel.d": {package: "node", module: "diagnostics_channel", path: "node/diagnostics_channel.d.ts" },
        "dns": {package: "node", module: "dns", path: "node/dns.d.ts" },
        "dns/promises": {package: "node", module: "dns/promises", path: "node/dns/promises.d.ts" },
        "domain": {package: "node", module: "domain", path: "node/domain.d.ts" },
        "events": {package: "node", module: "events", path: "node/events.d.ts" },
        "fs": {package: "node", module: "fs", path: "node/fs.d.ts" },
        "fs/promises": {package: "node", module: "fs/promises", path: "node/fs/promises.d.ts" },
        "globals": {package: "node", module: "globals", path: "node/globals.d.ts" },
        "http": {package: "node", module: "http", path: "node/http.d.ts" },
        "http2": {package: "node", module: "http2", path: "node/http2.d.ts" },
        "https": {package: "node", module: "https", path: "node/https.d.ts" },
        "module": {package: "node", module: "module", path: "node/module.d.ts" },
        "net": {package: "node", module: "net", path: "node/net.d.ts" },
        "os": {package: "node", module: "os", path: "node/os.d.ts" },
        "path": {package: "node", module: "path", path: "node/path.d.ts" },
        "perf_hooks": {package: "node", module: "perf_hooks", path: "node/perf_hooks.d.ts" },
        "process": {package: "node", module: "process", path: "node/process.d.ts" },
        "querystring": {package: "node", module: "querystring", path: "node/querystring.d.ts" },
        "readline": {package: "node", module: "readline", path: "node/readline.d.ts" },
        "stream": {package: "node", module: "stream", path: "node/stream.d.ts" },
        "stream/consumers": {package: "node", module: "stream/consumers", path: "node/stream/consumers.d.ts" },
        "stream/promises": {package: "node", module: "stream/promises", path: "node/stream/promises.d.ts" },
        "stream/web": {package: "node", module: "stream/web", path: "node/stream/web.d.ts" },
        "string_decoder": {package: "node", module: "string_decoder", path: "node/string_decoder.d.ts" },
        "test": {package: "node", module: "test", path: "node/test.d.ts" },
        "timers": {package: "node", module: "timers", path: "node/timers.d.ts" },
        "timers/promises": {package: "node", module: "timers/promises", path: "node/timers/promises.d.ts" },
        "tls": {package: "node", module: "tls", path: "node/tls.d.ts" },
        "trace_events": {package: "node", module: "trace_events", path: "node/trace_events.d.ts" },
        "tty": {package: "node", module: "tty", path: "node/tty.d.ts" },
        "url": {package: "node", module: "url", path: "node/url.d.ts" },
        "util": {package: "node", module: "util", path: "node/util.d.ts" },
        "v8": {package: "node", module: "v8", path: "node/v8.d.ts" },
        "vm": {package: "node", module: "vm", path: "node/vm.d.ts" },
        "wasi": {package: "node", module: "wasi", path: "node/wasi.d.ts" },
        "worker_threads": {package: "node", module: "worker_threads", path: "node/worker_threads.d.ts" },
        "zlib": {package: "node", module: "zlib", path: "node/zlib.d.ts" },
        "node-red": {package: "node-red", module: "node-red", path: "node-red/index.d.ts" }, //only used if node-red types are concated by grunt.
        "node-red-util": {package: "node-red", module: "util", path: "node-red/util.d.ts" },
        "node-red-func": {package: "node-red", module: "func", path: "node-red/func.d.ts" },
    }
    const defaultServerSideTypes = [ knownModules["node-red-util"], knownModules["node-red-func"], knownModules["globals"], knownModules["console"], knownModules["buffer"], knownModules["timers"], knownModules["url"] , knownModules["util"] ];

    const modulesCache = {};

    /**
     * Helper function to load/reload types.
     * @param {string} mod - type lib to load. Only known libs are currently supported.
     * @param {boolean} preloadOnly - only cache the lib
     * @param {object} loadedLibs - an object used to track loaded libs (needed to destroy them upon close)
     */
    function _loadModuleDTS(mod, preloadOnly, loadedLibs, cb) {
        var _module;
        if(typeof mod == "object") {
            _module = mod;
        } else {
            _module = knownModules[mod];
        }
        if(_module) {
            const libPackage = _module.package;
            const libModule = _module.module;
            const libPath = _module.path;
            const def = modulesCache[libPath];
            if( def ) {
                if(!preloadOnly) {
                    loadedLibs.JS[libModule] = monaco.languages.typescript.javascriptDefaults.addExtraLib(def, "file://types/" + libPackage + "/" + libModule + "/index.d.ts");
                }
                if(cb) {
                    setTimeout(function() {
                        cb(null, _module);
                    }, 5);
                }
            } else {
                var typePath = "types/" + libPath;
                $.get(typePath)
                .done(function(data) {
                    modulesCache[libPath] =  data;
                    if(!preloadOnly) {
                        loadedLibs.JS[libModule] = monaco.languages.typescript.javascriptDefaults.addExtraLib(data, "file://types/" + libPackage + "/" + libModule + "/index.d.ts");
                    }
                    if(cb) { cb(null, _module) }
                })
                .fail(function(err) {
                    var warning = "Failed to load '" + typePath + "'";
                    modulesCache[libPath] = "/* " + warning + " */\n"; //populate the extraLibs cache to revent retries
                    if(cb) { cb(err, _module) }
                    console.warn(warning);
                });
            }
        }
    }


    function init(options) {

        //Handles orphaned models
        //ensure loaded models that are not explicitly destroyed by a call to .destroy() are disposed
        RED.events.on("editor:close",function() {
            if (!window.monaco) { return; }
            const editors = window.monaco.editor.getEditors()
            const orphanEditors = editors.filter(editor => editor && !document.body.contains(editor.getDomNode()))
            orphanEditors.forEach(editor => {
                editor.dispose();
            });
            let models = monaco.editor.getModels()
            if(models && models.length) {
                console.warn("Cleaning up monaco models left behind. Any node that calls createEditor() should call .destroy().")
                for (let index = 0; index < models.length; index++) {
                    const model = models[index];
                    if(!model.isDisposed()) {
                        model.dispose();
                    }
                }
            }
        });

        options = options || {};
        window.MonacoEnvironment = window.MonacoEnvironment || {};
        window.MonacoEnvironment.getWorkerUrl = window.MonacoEnvironment.getWorkerUrl || function (moduleId, label) {
            if (label === 'json') { return './vendor/monaco/dist/json.worker.js'; }
            if (label === 'css' || label === 'scss') { return './vendor/monaco/dist/css.worker.js'; }
            if (label === 'html' || label === 'handlebars') { return './vendor/monaco/dist/html.worker.js'; }
            if (label === 'typescript' || label === 'javascript') { return './vendor/monaco/dist/ts.worker.js'; }
            return './vendor/monaco/dist/editor.worker.js';
        };

        var editorSettings = RED.editor.codeEditor.settings || {};
        var editorOptions = editorSettings.options || {};

        //if editorOptions.theme is an object (set in theme.js context()), use the plugin theme name as the monaco theme name
        //if editorOptions.theme is a string, it should be the name of a pre-set theme, load that
        try {
            const addTheme = function (themeThemeName, theme) {
                if ((theme.rules && Array.isArray(theme.rules)) || theme.colors) {
                    monacoThemes.push(themeThemeName); //add to list of loaded themes
                    monaco.editor.defineTheme(themeThemeName, theme);
                    monaco.editor.setTheme(themeThemeName);
                    userSelectedTheme = themeThemeName;
                }
            };
            if (editorOptions.theme) {
                if (typeof editorOptions.theme == "object" && RED.settings.editorTheme.theme) {
                    let themeThemeName = editorOptions.theme.name || RED.settings.editorTheme.theme;
                    addTheme(themeThemeName, editorOptions.theme);
                } else if (typeof editorOptions.theme == "string") {
                    let themeThemeName = editorOptions.theme;
                    if (!monacoThemes.includes(themeThemeName)) {
                        $.get('vendor/monaco/dist/theme/' + themeThemeName + '.json', function (theme) {
                            addTheme(themeThemeName, theme);
                        });
                    }
                }
            }
        } catch (error) {
            console.warn(error);
        }


        //Helper function to simplify snippet setup
        function createMonacoCompletionItem(label, insertText, documentation, range, kind) {
            if (Array.isArray(documentation)) { documentation = documentation.join("\n"); }
            return {
                label: label,
                kind: kind == null ? monaco.languages.CompletionItemKind.Snippet : kind,
                documentation: { value: documentation },
                insertText: insertText,
                insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
                range: range
            }
        }

        function setupJSONata(_monaco) {
            // Register the language 'jsonata'
            _monaco.languages.register({ id: 'jsonata' });

            // Setup tokens for JSONata
            _monaco.languages.setMonarchTokensProvider('jsonata',
                {
                    // Set defaultToken to invalid to see what you do not tokenize yet
                    defaultToken: 'invalid',
                    tokenPostfix: '.js',
                    keywords: ["function", "true", "true", "null", "Infinity", "NaN", "undefined"].concat(Object.keys(jsonata.functions)),
                    // keywords: [
                    //     "function", "$abs", "$append", "$assert", "$average",
                    //     "$base64decode", "$base64encode", "$boolean", "$ceil", "$contains",
                    //     "$count", "$decodeUrl", "$decodeUrlComponent", "$distinct", "$each", "$encodeUrl",
                    //     "$encodeUrlComponent", "$env", "$error", "$eval", "$exists", "$filter", "$floor",
                    //     "$flowContext", "$formatBase", "$formatInteger", "$formatNumber", "$fromMillis",
                    //     "$globalContext", "$join", "$keys", "$length", "$lookup", "$lowercase", "$map",
                    //     "$match", "$max", "$merge", "$millis", "$min", "$moment", "$not", "$now",
                    //     "$number", "$pad", "$parseInteger", "$power", "$random", "$reduce", "$replace",
                    //     "$reverse", "$round", "$shuffle", "$sift", "$single", "$sort", "$split",
                    //     "$spread", "$sqrt", "$string", "$substring", "$substringAfter", "$substringBefore",
                    //     "$sum", "$toMillis", "$trim", "$type", "$uppercase", "$zip"
                    // ],

                    operatorsKeywords: [ 'and', 'or', 'in' ],

                    operators: [
                        '<=', '>=', '!=', '==', '!=', '=>', '+', '-', '*', '/', '%',
                        ':=', '~>', '?', ':', '..', '@', '#', '|', '^', '*', '**',
                    ],

                    // we include these common regular expressions
                    symbols: /[=><!~?:&|+\-*\/\^%@#]+/,
                    escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
                    digits: /\d+(_+\d+)*/,
                    octaldigits: /[0-7]+(_+[0-7]+)*/,
                    binarydigits: /[0-1]+(_+[0-1]+)*/,
                    hexdigits: /[[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/,

                    regexpctl: /[(){}\[\]\$\^|\-*+?\.]/,
                    regexpesc: /\\(?:[bBdDfnrstvwWn0\\\/]|@regexpctl|c[A-Z]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4})/,

                    // The main tokenizer
                    tokenizer: {
                        root: [
                            [/[{}]/, 'delimiter.bracket'],
                            { include: 'common' }
                        ],

                        common: [
                            // identifiers and keywords
                            [/([a-zA-Z][\w$]*)|([$][\w$]*)/, {
                                cases: {
                                    '@keywords': 'keyword',
                                    '@operatorsKeywords': 'keyword',
                                    '$2': 'variable', //when its not a key word but starts with $, use "tag" for colourisation
                                    //'$2': 'tag', //when its not a key word but starts with $, use "tag" for colourisation
                                    //'$2': 'constant', //alt colourisation
                                    //'$2': 'attribute', //alt colourisation
                                    //'$2': 'identifier.variable', //alt custom colourisation
                                    '@default': 'identifier'
                                }
                            }],
                            [/[$][\w\$]*/, 'variable'],

                            // whitespace
                            { include: '@whitespace' },

                            // regular expression: ensure it is terminated before beginning (otherwise it is an opeator)
                            [/\/(?=([^\\\/]|\\.)+\/([gimsuy]*)(\s*)(\.|;|\/|,|\)|\]|\}|$))/, { token: 'regexp', bracket: '@open', next: '@regexp' }],

                            // delimiters and operators
                            [/[()\[\]]/, '@brackets'],
                            [/[<>](?!@symbols)/, '@brackets'],
                            [/(@symbols)|(\.\.)/, {
                                cases: {
                                    '@operators': 'operator',
                                    '@default': ''
                                }
                            }],

                            // numbers
                            [/(@digits)[eE]([\-+]?(@digits))?/, 'number.float'],
                            [/(@digits)\.(@digits)([eE][\-+]?(@digits))?/, 'number.float'],
                            [/0[xX](@hexdigits)/, 'number.hex'],
                            [/0[oO]?(@octaldigits)/, 'number.octal'],
                            [/0[bB](@binarydigits)/, 'number.binary'],
                            [/(@digits)/, 'number'],

                            // delimiter: after number because of .\d floats
                            [/[?:;,.]/, 'delimiter'],

                            // strings
                            [/"([^"\\]|\\.)*$/, 'string.invalid'],  // non-teminated string
                            [/'([^'\\]|\\.)*$/, 'string.invalid'],  // non-teminated string
                            [/"/, 'string', '@string_double'],
                            [/'/, 'string', '@string_single'],
                            [/`/, 'string', '@string_backtick'],
                        ],

                        whitespace: [
                            [/[ \t\r\n]+/, ''],
                            [/\/\*\*(?!\/)/, 'comment.doc', '@jsdoc'],
                            [/\/\*/, 'comment', '@comment'],
                            [/\/\/.*$/, 'comment'],
                        ],

                        comment: [
                            [/[^\/*]+/, 'comment'],
                            [/\*\//, 'comment', '@pop'],
                            [/[\/*]/, 'comment']
                        ],

                        jsdoc: [
                            [/[^\/*]+/, 'comment.doc'],
                            [/\*\//, 'comment.doc', '@pop'],
                            [/[\/*]/, 'comment.doc']
                        ],

                        // We match regular expression quite precisely
                        regexp: [
                            [/(\{)(\d+(?:,\d*)?)(\})/, ['regexp.escape.control', 'regexp.escape.control', 'regexp.escape.control']],
                            [/(\[)(\^?)(?=(?:[^\]\\\/]|\\.)+)/, ['regexp.escape.control', { token: 'regexp.escape.control', next: '@regexrange' }]],
                            [/(\()(\?:|\?=|\?!)/, ['regexp.escape.control', 'regexp.escape.control']],
                            [/[()]/, 'regexp.escape.control'],
                            [/@regexpctl/, 'regexp.escape.control'],
                            [/[^\\\/]/, 'regexp'],
                            [/@regexpesc/, 'regexp.escape'],
                            [/\\\./, 'regexp.invalid'],
                            [/(\/)([gimsuy]*)/, [{ token: 'regexp', bracket: '@close', next: '@pop' }, 'keyword.other']],
                        ],

                        regexrange: [
                            [/-/, 'regexp.escape.control'],
                            [/\^/, 'regexp.invalid'],
                            [/@regexpesc/, 'regexp.escape'],
                            [/[^\]]/, 'regexp'],
                            [/\]/, { token: 'regexp.escape.control', next: '@pop', bracket: '@close' }],
                        ],

                        string_double: [
                            [/[^\\"]+/, 'string'],
                            [/@escapes/, 'string.escape'],
                            [/\\./, 'string.escape.invalid'],
                            [/"/, 'string', '@pop']
                        ],

                        string_single: [
                            [/[^\\']+/, 'string'],
                            [/@escapes/, 'string.escape'],
                            [/\\./, 'string.escape.invalid'],
                            [/'/, 'string', '@pop']
                        ],

                        string_backtick: [
                            [/\$\{/, { token: 'delimiter.bracket', next: '@bracketCounting' }],
                            [/[^\\`$]+/, 'string'],
                            [/@escapes/, 'string.escape'],
                            [/\\./, 'string.escape.invalid'],
                            [/`/, 'string', '@pop']
                        ],

                        bracketCounting: [
                            [/\{/, 'delimiter.bracket', '@bracketCounting'],
                            [/\}/, 'delimiter.bracket', '@pop'],
                            { include: 'common' }
                        ],
                    },
                }

            );

           // Setup JSONata language config
            _monaco.languages.setLanguageConfiguration('jsonata', {
                comments: {
                    lineComment: '//',
                    blockComment: ['/*', '*/']
                },
                brackets: [
                    ['{', '}'],
                    ['[', ']'],
                    ['(', ')']
                ],
                autoClosingPairs: [
                    { open: '{', close: '}' },
                    { open: '[', close: ']' },
                    { open: '(', close: ')' },
                    { open: "'", close: "'", notIn: ['string', 'comment'] },
                    { open: '"', close: '"', notIn: ['string'] }
                ],
                surroundingPairs: [
                    { open: '{', close: '}' },
                    { open: '[', close: ']' },
                    { open: '(', close: ')' },
                    { open: '"', close: '"' },
                    { open: "'", close: "'" },
                    { open: '<', close: '>' }
                ],
                folding: {
                    markers: {
                        start: new RegExp('^\\s*//\\s*(?:(?:#?region\\b)|(?:<editor-fold\\b))'),
                        end: new RegExp('^\\s*//\\s*(?:(?:#?endregion\\b)|(?:</editor-fold>))')
                    }
                }
            });

            // Register a completion item provider for JSONata snippets
            _monaco.languages.registerCompletionItemProvider('jsonata', {
                provideCompletionItems: function (model, position) {
                    var _word = model.getWordUntilPosition(position);
                    if (!_word) { return; }
                    var startColumn = _word.startColumn;
                    var word = _word.word;
                    if (word[0] !== "$" && position.column > 1) { startColumn--; }
                    var range = {
                        startLineNumber: position.lineNumber,
                        endLineNumber: position.lineNumber,
                        startColumn: startColumn,
                        endColumn: _word.endColumn
                    };
                    var jsonataFunctions = Object.keys(jsonata.functions);
                    var jsonataSuggestions = jsonataFunctions.map(function (f) {
                        var args = RED._('jsonata:' + f + '.args', { defaultValue: '' });
                        var title = f + '(' + args + ')';
                        var body = RED._('jsonata:' + f + '.desc', { defaultValue: '' });
                        var insertText = (jsonata.getFunctionSnippet(f) + '').trim();
                        var documentation = { value: '`' + title + '`\n\n' + body };
                        return createMonacoCompletionItem(f, insertText, documentation, range, monaco.languages.CompletionItemKind.Function);
                    });
                    // sort in length order (long->short) otherwise substringAfter gets matched as substring etc.
                    jsonataFunctions.sort(function (A, B) {
                        return B.length - A.length;
                    });
                    // add snippets to suggestions
                    jsonataSuggestions.unshift(
                        createMonacoCompletionItem("randominteger", '(\n\t\\$minimum := ${1:1};\n\t\\$maximum := ${2:10};\n\t\\$round((\\$random() * (\\$maximum-\\$minimum)) + \\$minimum, 0)\n)', 'Random integer between 2 numbers', range)
                    );//TODO: add more JSONata snippets
                    return { suggestions: jsonataSuggestions };
                }
            });

            // Register a hover provider for JSONata functions
            _monaco.languages.registerHoverProvider('jsonata', {
                provideHover: function (model, position) {
                    var w = model.getWordAtPosition(position);
                    var f = w && w.word;
                    if (!f) {return;}
                    if (f[0] !== "$" && position.column > 1) {
                        f = "$" + f;
                    } else {
                        return;
                    }
                    var args = RED._('jsonata:' + f + ".args", { defaultValue: '' });
                    if (!args) {return;}
                    var title = f + "(" + args + ")";
                    var body = RED._('jsonata:' + f + '.desc', { defaultValue: '' });

                    /** @type {monaco.Range} */
                    var r = new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column + w.word.length);
                    return {
                        range: r,
                        contents: [
                            { value: '**`' + title + '`**' },
                            // { value: '```html\n' + body + '\n```' },
                            { value: body },
                        ]
                    }
                }
            });
        }

        function setupJSON(_monaco) {
            //Setup JSON options
            try {
                var diagnosticOptionsDefault = {validate: true};
                var diagnosticOptions = RED.settings.get('codeEditor.monaco.languages.json.jsonDefaults.diagnosticOptions');
                var modeConfiguration = RED.settings.get('codeEditor.monaco.languages.json.jsonDefaults.modeConfiguration');
                diagnosticOptions = Object.assign({}, diagnosticOptionsDefault, (diagnosticOptions || {}));
                _monaco.languages.json.jsonDefaults.setDiagnosticsOptions(diagnosticOptions);
                if(modeConfiguration) { _monaco.languages.json.jsonDefaults.setModeConfiguration(modeConfiguration); }
            } catch (error) {
                console.warn("monaco - Error setting up json options", error)
            }
        }

        function setupHTML(_monaco) {
            //Setup HTML / Handlebars options
            try {
                var htmlDefaults = RED.settings.get('codeEditor.monaco.languages.html.htmlDefaults.options');
                var handlebarDefaults = RED.settings.get('codeEditor.monaco.languages.html.handlebarDefaults.options');
                if(htmlDefaults) { _monaco.languages.html.htmlDefaults.setOptions(htmlDefaults); }
                if(handlebarDefaults) { _monaco.languages.html.handlebarDefaults.setOptions(handlebarDefaults); }
            } catch (error) {
                console.warn("monaco - Error setting up html options", error)
            }
        }

        function setupCSS(_monaco) {
            //Setup CSS/SCSS/LESS options
            try {
                var cssDefaults_diagnosticsOption = RED.settings.get('codeEditor.monaco.languages.css.cssDefaults.diagnosticsOptions');
                var lessDefaults_diagnosticsOption = RED.settings.get('codeEditor.monaco.languages.css.lessDefaults.diagnosticsOption');
                var scssDefaults_diagnosticsOption = RED.settings.get('codeEditor.monaco.languages.css.scssDefaults.diagnosticsOption');
                var cssDefaults_modeConfiguration = RED.settings.get('codeEditor.monaco.languages.css.cssDefaults.modeConfiguration');
                var lessDefaults_modeConfiguration = RED.settings.get('codeEditor.monaco.languages.css.lessDefaults.modeConfiguration');
                var scssDefaults_modeConfiguration = RED.settings.get('codeEditor.monaco.languages.css.scssDefaults.modeConfiguration');
                if(cssDefaults_diagnosticsOption) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(cssDefaults_diagnosticsOption); }
                if(lessDefaults_diagnosticsOption) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(lessDefaults_diagnosticsOption); }
                if(scssDefaults_diagnosticsOption) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(scssDefaults_diagnosticsOption); }
                if(cssDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(cssDefaults_modeConfiguration); }
                if(lessDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(lessDefaults_modeConfiguration); }
                if(scssDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(scssDefaults_modeConfiguration); }
            } catch (error) {
                console.warn("monaco - Error setting up CSS/SCSS/LESS options", error)
            }
        }

        function setupJS(_monaco) {
            var createJSSnippets = function(range) {
                return [
                    //TODO: i18n for snippet descriptions?
                    createMonacoCompletionItem("dowhile", 'do {\n\t${2}\n} while (${1:condition});','Do-While Statement (JavaScript Language Basics)',range),
                    createMonacoCompletionItem("while", 'while (${1:condition}) {\n\t${2}\n}','While Statement (JavaScript Language Basics)',range),
                    createMonacoCompletionItem("switch", 'switch (${1:msg.topic}) {\n\tcase ${2:"value"}:\n\t\t${3}\n\t\tbreak;\n\tdefault:\n\t\t\n}','Switch Statement (JavaScript Language Basics)',range),
                    createMonacoCompletionItem("trycatch", 'try {\n\t${2}\n} catch (${1:error}) {\n\t\n};','Try-Catch Statement (JavaScript Language Basics)',range),
                    createMonacoCompletionItem("for (for loop)", 'for (let ${1:index} = 0; ${1:index} < ${2:array}.length; ${1:index}++) {\n\tconst element = ${2:array}[${1:index}];\n\t${3}\n}','for loop',range),
                    createMonacoCompletionItem("foreach", '${1:array}.forEach(function(${2:element}) {\n\t${3}\n});','forEach(callbackfn: (value: T, index: number, array: readonly T[]) => void, thisArg?: any): void\n\nA function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array.',range),
                    createMonacoCompletionItem("forin", 'for (${1:prop} in ${2:obj}) {\n\tif (${2:obj}.hasOwnProperty(${1:prop})) {\n\t\t${3}\n\t}\n}','for in',range),
                    createMonacoCompletionItem("forof", 'for (const ${1:iterator} of ${2:object}) {\n\t${3}\n}','for of',range),
                    createMonacoCompletionItem("function", 'function ${1:methodName}(${2:arguments}) {\n\t${3}\n}','Function Declaration',range),
                    createMonacoCompletionItem("func (anonymous function)", 'var ${1:fn} = function(${2:arguments}) {\n\t${3}\n}','Function Expression',range),
                    createMonacoCompletionItem("pt (prototype)", '${1:ClassName}.prototype.${2:methodName} = function(${3:arguments}) {\n\t${4}\n}','prototype',range),
                    createMonacoCompletionItem("iife", '(function(${1:arg}) {\n\t${1}\n})(${1:arg});','immediately-invoked function expression',range),
                    createMonacoCompletionItem("call (function call)", '${1:methodName}.call(${2:context}, ${3:arguments})','function call',range),
                    createMonacoCompletionItem("apply (function apply)", '${1:methodName}.apply(${2:context}, [${3:arguments}])','function apply',range),
                    createMonacoCompletionItem("jsonparse", 'JSON.parse(${1:json});','JSON.parse',range),
                    createMonacoCompletionItem("jsonstringify", 'JSON.stringify(${1:obj});','JSON.stringify',range),
                    createMonacoCompletionItem("setinterval", 'setInterval(function() {\n\t${2}\n}, ${1:delay});','setInterval',range),
                    createMonacoCompletionItem("settimeout", 'setTimeout(function() {\n\t${2}\n}, ${1:delay});','setTimeout',range),
                    createMonacoCompletionItem("node.log", 'node.log(${1:"info"});','Write an info message to the console (not sent to sidebar)',range),
                    createMonacoCompletionItem("node.warn", 'node.warn(${1:"my warning"});','Write a warning to the console and debug sidebar',range),
                    createMonacoCompletionItem("node.error", 'node.error(${1:"my error message"}, ${2:msg});','Send an error to the console and debug sidebar. To trigger a Catch node on the same tab, the function should call `node.error` with the original message as a second argument',range),
                    createMonacoCompletionItem("node.send", 'node.send(${1:msg});','async send a msg to the next node',range),
                    createMonacoCompletionItem("node.send (multiple)", 'var ${1:msg1} = {payload:${2:1}};\nvar ${3:msg2} = {payload:${4:2}};\nnode.send([[${1:msg1}, ${3:msg2}]]);','send 1 or more messages out of 1 output',range),
                    createMonacoCompletionItem("node.send (multiple outputs)", 'var ${1:msg1} = {payload:${2:1}};\nvar ${3:msg2} = {payload:${4:2}};\nnode.send([${1:msg1}, ${3:msg2}]);','send more than 1 message out of multiple outputs',range),
                    createMonacoCompletionItem("node.status", 'node.status({fill:"${1|red,green,yellow,blue,grey|}",shape:"${2|ring,dot|}",text:"${3:message}"});','Set the status icon and text underneath the function node',range),
                    createMonacoCompletionItem("get (node context)", 'context.get("${1:name}");','Get a value from node context',range),
                    createMonacoCompletionItem("set (node context)", 'context.set("${1:name}", ${1:value});','Set a value in node context',range),
                    createMonacoCompletionItem("get (flow context)", 'flow.get("${1:name}");','Get a value from flow context',range),
                    createMonacoCompletionItem("set (flow context)", 'flow.set("${1:name}", ${1:value});','Set a value in flow context',range),
                    createMonacoCompletionItem("get (global context)", 'global.get("${1:name}");','Get a value from global context',range),
                    createMonacoCompletionItem("set (global context)", 'global.set("${1:name}", ${1:value});','Set a value in global context',range),
                    createMonacoCompletionItem("get (env)", 'env.get("${1|NR_NODE_ID,NR_NODE_NAME,NR_NODE_PATH,NR_GROUP_ID,NR_GROUP_NAME,NR_FLOW_ID,NR_FLOW_NAME,NR_SUBFLOW_NAME,NR_SUBFLOW_ID,NR_SUBFLOW_PATH|}");','Get env variable value',range),
                    createMonacoCompletionItem("cloneMessage (RED.util)", 'RED.util.cloneMessage(${1:msg});',
                        ["```typescript",
                        "RED.util.cloneMessage<T extends registry.NodeMessage>(msg: T): T",
                        "```",
                        "Safely clones a message object. This handles msg.req/msg.res objects that must not be cloned\n",
                        "*@param* `msg` — the msg object\n"],
                        range),
                    createMonacoCompletionItem("getObjectProperty (RED.util)", 'RED.util.getObjectProperty(${1:msg},${2:prop});',
                        ["```typescript",
                        "RED.util.getObjectProperty(msg: object, expr: string): any;",
                        "```",
                        "Gets a property of an object\n",
                        "*@param* `msg` — the msg object\n",
                        "*@param* `prop` — the msg object"],
                        range),
                    createMonacoCompletionItem("setObjectProperty (RED.util)", 'RED.util.setObjectProperty(${1:msg},${2:prop},${3:value},${4:createMissing});',
                        ["```typescript",
                        "RED.util.setObjectProperty(msg: object, prop: string, value: any, createMissing?: boolean): boolean",
                        "```",
                        "Sets a property of an object\n",
                        "`msg` — the object\n",
                        "`prop` — the property expression\n",
                        "`value` — the value to set\n",
                        "`createMissing` — whether to create missing parent properties"],
                        range),
                    createMonacoCompletionItem("getMessageProperty (RED.util)", 'RED.util.getMessageProperty(${1:msg},${2:prop});',
                        ["```typescript",
                        "RED.util.getMessageProperty(msg: object, expr: string): any;",
                        "```",
                        "Gets a property of an object\n",
                        "*@param* `msg` — the msg object\n",
                        "*@param* `prop` — the msg object"],
                        range),
                    createMonacoCompletionItem("setMessageProperty (RED.util)", 'RED.util.setMessageProperty(${1:msg},${2:prop},${3:value},${4:createMissing});',
                        ["```typescript",
                        "RED.util.setMessageProperty(msg: object, prop: string, value: any, createMissing?: boolean): boolean",
                        "```",
                        "Sets a property of an object\n",
                        "`msg` — the object\n",
                        "`prop` — the property expression\n",
                        "`value` — the value to set\n",
                        "`createMissing` — whether to create missing parent properties"],
                        range),
                ];
            }

            //register snippets
            _monaco.languages.registerCompletionItemProvider('javascript', {
                provideCompletionItems: function(model, position) {
                    var word = model.getWordUntilPosition(position);
                    var range = {
                        startLineNumber: position.lineNumber,
                        endLineNumber: position.lineNumber,
                        startColumn: word.startColumn,
                        endColumn: word.endColumn
                    };
                    return {
                        suggestions: createJSSnippets(range)
                    };
                }
            });

            //setup JS/TS compiler & diagnostic options (defaults)
            try {
                //prepare compiler options
                var compilerOptions = {
                    allowJs: true,
                    checkJs: true,
                    allowNonTsExtensions: true,
                    target: monaco.languages.typescript.ScriptTarget.ESNext,
                    strictNullChecks: false,
                    strictPropertyInitialization: true,
                    strictFunctionTypes: true,
                    strictBindCallApply: true,
                    useDefineForClassFields: true,//permit class static fields with private name to have initializer
                    moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
                    module: monaco.languages.typescript.ModuleKind.CommonJS,
                    typeRoots: ["types"],
                    lib: ["esnext"] //dont load DOM by default,
                }
                //apply overrides from codeEditor.monaco.languages.typescript.javascriptDefaults.compilerOptions in settings.js
                var settingsComilerOptions = RED.settings.get('codeEditor.monaco.languages.typescript.javascriptDefaults.compilerOptions') || {};
                compilerOptions = Object.assign({}, compilerOptions, settingsComilerOptions);
                /** @see https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.typescript.languageservicedefaults.html#setcompileroptions */
                _monaco.languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions);

                //prepare diagnostic options (defaults)
                var diagnosticOptions = {
                    noSemanticValidation: false,
                    noSyntaxValidation: false,
                    diagnosticCodesToIgnore:  [
                        1108,  //return not inside function
                        1375,  //'await' expressions are only allowed at the top level of a file when that file is a module
                        1378,  //Top-level 'await' expressions are only allowed when the 'module' option is set to 'esnext' or 'system', and the 'target' option is set to 'es2017' or higher
                        //2304,  //Cannot find name - this one is heavy handed and prevents user seeing stupid errors. Would provide better ACE feature parity (i.e. no need for declaration of vars) but misses lots of errors. Lets be bold & leave it out!
                        2307,  //Cannot find module 'xxx' or its corresponding type declarations
                        2322,  //Type 'unknown' is not assignable to type 'string'
                        2339,  //property does not exist on
                        2345,  //Argument of type xxx is not assignable to parameter of type 'DateTimeFormatOptions'
                        2538,  //Ignore symbols as index property error. 
                        7043,  //i forget what this one is,
                        80001, //Convert to ES6 module
                        80004, //JSDoc types may be moved to TypeScript types.
                    ]
                };
                //apply overrides from codeEditor.monaco.languages.typescript.javascriptDefaults.diagnosticsOptions settings.js
                var settingsDiagnosticsOptions = RED.settings.get('codeEditor.monaco.languages.typescript.javascriptDefaults.diagnosticsOptions') || {};
                diagnosticOptions = Object.assign({}, diagnosticOptions, settingsDiagnosticsOptions);
                /** @see https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.typescript.languageservicedefaults.html#setdiagnosticsoptions */
                _monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions(diagnosticOptions);
            } catch (error) {
                console.warn("monaco - Error setting javascriptDefaults", error)
            }
        }

        setupJS(monaco);
        setupJSONata(monaco);
        setupJSON(monaco);
        setupCSS(monaco);
        setupHTML(monaco);
        defaultServerSideTypes.forEach(function(m) {
            _loadModuleDTS(m, true)//pre-load common libs
        })

        initialised = true;
        return initialised;
    }

    function create(options) {

        var editorSettings = RED.editor.codeEditor.settings || {};
        var loadedLibs = {JS:{}, TS:{}};//for tracking and later disposing of loaded type libs
        var watchTimer;
        var createThemeMenuOption = function (theme, keybinding) {
            return {
                // An unique identifier of the contributed action.
                id: 'set-theme-' + theme,
                // A label of the action that will be presented to the user.
                label: RED._('monaco.setTheme') + ' ' + theme,
                precondition: null,// A precondition for this action.
                keybindingContext: keybinding || null,// A rule to evaluate on top of the precondition in order to dispatch the keybindings.

                // Method that will be executed when the action is triggered.
                // @param editor The editor instance is passed in as a convinience
                run: function (ed) {
                    //monaco.editor.setTheme(theme)
                    ed.setTheme(theme)
                    return null;
                }
            }
        }

        var convertAceModeToMonacoLang = function (mode) {
            if (typeof mode == "object" && mode.path) {
                mode = mode.path;
            }
            if (mode) {
                mode = mode.replace("ace/mode/", "");
            } else {
                mode = "text";
            }
            switch (mode) {
                case "nrjavascript":
                case "mjs":
                    mode = "javascript";
                    break;
                case "vue":
                    mode = "html";
                    break;
                case "appcache":
                case "sh":
                case "bash":
                    mode = "shell";
                    break;
                case "batchfile":
                    mode = "bat";
                    break;
                case "protobuf":
                    mode = "proto";
                    break;
                //TODO: add other compatability types.
            }
            return mode;
        }

        
        if(!options.stateId && options.stateId !== false) {
            options.stateId = RED.editor.generateViewStateId("monaco", options, (options.mode || options.title || "").split("/").pop());
        }
        var el = options.element || $("#"+options.id)[0];
        var toolbarRow = $("<div>").appendTo(el);
        el = $("<div>").appendTo(el).addClass("red-ui-editor-text-container")[0];

        var editorOptions = $.extend({}, editorSettings.options, options);
        editorOptions.language = convertAceModeToMonacoLang(options.mode);

        if(userSelectedTheme) {
            editorOptions.theme = userSelectedTheme;//use user selected theme for this session
        }

        //by default, set javascript editors to text mode.
        //when element becomes visible, it will be (re) set to javascript mode
        //this is to ensure multiple editors sharing the model dont present its
        //consts & lets to each other
        if(editorOptions.language == "javascript") {
            editorOptions._language = editorOptions.language;
            editorOptions.language = "text"
        }

        //apply defaults
        if (!editorOptions.minimap) {
            editorOptions.minimap = {
                enabled: true,
                maxColumn: 50,
                scale: 1,
                showSlider: "mouseover",
                renderCharacters: true
            }
        }

        //common ACE flags - set equivelants in monaco
        if(options.enableBasicAutocompletion === false) {
            editorOptions.showSnippets = false;
            editorOptions.quickSuggestions = false;
            editorOptions.parameterHints = { enabled: false };
            editorOptions.suggestOnTriggerCharacters = false;
            editorOptions.acceptSuggestionOnEnter = "off";
            editorOptions.tabCompletion = "off";
            editorOptions.wordBasedSuggestions = false;
        }
        if (options.enableSnippets === false) { editorOptions.showSnippets = false; }
        if (editorOptions.mouseWheelZoom == null) { editorOptions.mouseWheelZoom = true; }
        if (editorOptions.suggestFontSize == null) { editorOptions.suggestFontSize = 12; }
        if (editorOptions.formatOnPaste == null) { editorOptions.formatOnPaste = true; }
        if (editorOptions.foldingHighlight == null) { editorOptions.foldingHighlight = true; }
        if (editorOptions.foldStyle == null) { editorOptions.foldStyle = true; } //https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html#folding
        if (editorOptions.readOnly != null) { editorOptions.readOnly = editorOptions.readOnly; }
        if (editorOptions.lineNumbers === false) { editorOptions.lineNumbers = false; }
        if (editorOptions.theme == null) { editorOptions.theme = monacoThemes[0]; }
        if (editorOptions.mode == null) { editorOptions.mode = convertAceModeToMonacoLang(options.mode); }
        if (editorOptions.automaticLayout == null) { editorOptions.automaticLayout = true; }

        if (options.foldStyle) {
            switch (options.foldStyle) {
                case "none":
                    editorOptions.foldStyle = false;
                    editorOptions.foldingHighlight = false
                    break;
                default:
                    editorOptions.foldStyle = true;
                    editorOptions.foldingHighlight = true;
                    break;
            }
        } else {
            editorOptions.foldStyle = true;
            editorOptions.foldingHighlight = true;
        }

        //others
        editorOptions.roundedSelection = editorOptions.roundedSelection === false ? false : true; //default to true
        editorOptions.contextmenu = editorOptions.contextmenu === false ? false : true; //(context menu enable) default to true
        editorOptions.snippetSuggestions = editorOptions.enableSnippets === false ? false : true; //default to true //https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html#snippetsuggestions

        editorOptions.value = options.value || "";

        //if wordSeparators are not supplied, override default ones for the "j" langs
        if(!editorOptions.wordSeparators) {
            if (editorOptions.language == "jsonata" || editorOptions.language == "json" || editorOptions.language == "javascript") {
                editorOptions.wordSeparators = "`~!@#%^&*()-=+[{]}\|;:'\",.<>/?"; //dont use $ as separator
            }
        }

        //fixedOverflowWidgets should probably never be set to false
        //fixedOverflowWidgets allows hover tips to be above other parts of UI
        editorOptions.fixedOverflowWidgets = editorOptions.fixedOverflowWidgets === false ? false : true;

        //#region Detect mobile / tablet and reduce functionality for small screen and lower power
        var browser = RED.utils.getBrowserInfo();
        if (browser.mobile || browser.tablet) {
            editorOptions.minimap = { enabled: false };
            editorOptions.formatOnType = false; //try to prevent cursor issues
            editorOptions.formatOnPaste = false; //try to prevent cursor issues
            editorOptions.disableMonospaceOptimizations = true; //speed up
            editorOptions.columnSelection = false; //try to prevent cursor issues
            editorOptions.matchBrackets = "never"; //speed up
            editorOptions.maxTokenizationLineLength = 10000; //speed up //internal default is 20000
            editorOptions.stopRenderingLineAfter = 2000; //speed up //internal default is 10000
            editorOptions.roundedSelection = false; //speed up rendering
            editorOptions.trimAutoWhitespace = false; //try to prevent cursor issues
            editorOptions.parameterHints = { enabled: false };
            editorOptions.suggestOnTriggerCharacters = false;//limit suggestions, user can still use ctrl+space
            editorOptions.wordBasedSuggestions = false;
            editorOptions.suggest = { maxVisibleSuggestions: 6 };
            // editorOptions.codeLens = false;//If Necessary, disable this useful feature?
            // editorOptions.quickSuggestions = false;//If Necessary, disable this useful feature?
            // editorOptions.showSnippets = false; //If Necessary, disable this useful feature?
            // editorOptions.acceptSuggestionOnEnter = "off"; //If Necessary, disable this useful feature?
            // editorOptions.tabCompletion = "off"; //If Necessary, disable this useful feature?
            if (!editorOptions.accessibilitySupport && browser.android) {
                editorOptions.accessibilitySupport = "off"; //ref https://github.com/microsoft/pxt/pull/7099/commits/35fd3e969b0d5b68ca1e35809f96cea81ef243bc
            }
        }
        //#endregion

        //#region Load types for intellisense

        //Determine if this instance is for client side or server side...
        //if clientSideSuggestions option is not specified, check see if
        //requested mode is "nrjavascript" or if options.globals are specifying RED or Buffer
        var serverSideSuggestions = false;
        if (options.clientSideSuggestions == null) {
            if ( ((options.mode + "").indexOf("nrjavascript") >= 0) || (options.globals && (options.globals.RED || options.globals.Buffer )) ) {
                serverSideSuggestions = true;
            }
        }

        // compiler options - enable / disable server-side/client-side suggestions
        var compilerOptions = monaco.languages.typescript.javascriptDefaults.getCompilerOptions();
        if (serverSideSuggestions) {
            compilerOptions.lib = ["esnext"]; //dont include DOM
            defaultServerSideTypes.forEach(function(m) {
                _loadModuleDTS(m, false, loadedLibs); //load common node+node-red types
            })
        } else {
            compilerOptions.lib = ["esnext", "dom"];
        }

        monaco.languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions);

        //check if extraLibs are to be loaded (e.g. fs or os)
        refreshModuleLibs(editorOptions.extraLibs)

        function refreshModuleLibs(extraModuleLibs) {
            var defs = [];
            var imports = [];
            const id = "extraModuleLibs/index.d.ts";
            const file = 'file://types/extraModuleLibs/index.d.ts';
            if(!extraModuleLibs || extraModuleLibs.length == 0) {
                loadedLibs.JS[id] = monaco.languages.typescript.javascriptDefaults.addExtraLib(" ", file);
            } else {
                var loadList = [];
                Array.prototype.push.apply(loadList, extraModuleLibs);//Use this instead of spread operator to prevent IE syntax error
                var loadExtraModules = {};
                for (let index = 0; index < extraModuleLibs.length; index++) {
                    const lib = extraModuleLibs[index];
                    const varName = lib.var;
                    const moduleName = lib.module;
                    if(varName && moduleName) {
                        imports.push("import " + varName + "_import = require('" + moduleName + "');\n");
                        defs.push("var " + varName + ": typeof " + varName + "_import;\n");
                    }
                    var km = knownModules[moduleName];
                    if(km) {
                        loadExtraModules[moduleName] = km;
                    } else {
                        loadExtraModules[moduleName] = {package: "other", module: moduleName, path: "other/" +  moduleName + ".d.ts" };
                    }
                }
                Object.values(loadExtraModules).forEach(function(m) {
                    _loadModuleDTS(m, false, loadedLibs, function(err, extraLib) {
                        loadList = loadList.filter(function(e) {return e.module != extraLib.module} );
                        if(loadList.length == 0) {
                            var _imports = imports.join("");
                            var _defs  = "\ndeclare global {\n" + defs.join("") + "\n}";
                            var libSource = _imports + _defs;
                            setTimeout(function() {
                                loadedLibs.JS[id] = monaco.languages.typescript.javascriptDefaults.addExtraLib(libSource, file);
                            }, 500);
                        }
                    });
                });
            }
        }

        //#endregion

        /*********** Create the monaco editor ***************/
        var ed = monaco.editor.create(el, editorOptions);

        //Unbind ctrl-Enter (default action is to insert a newline in editor) This permits the shortcut to close the tray.
        try {
            monaco.editor.addKeybindingRule({keybinding: 0, command: "-editor.action.insertLineAfter"});
        } catch (error) {
            console.warn(error)
        }

        ed.nodered = {
            refreshModuleLibs: refreshModuleLibs //expose this for function node externalModules refresh
        }

        //add f1 menu items for changing theme
        for (var themeIdx = 0; themeIdx < monacoThemes.length; themeIdx++) {
            var themeName = monacoThemes[themeIdx];
            ed.addAction(createThemeMenuOption(themeName));
        }

        //#region "ACE compatability"

        ed.selection = {};
        ed.session = ed;
        ed.renderer = {};

        ed.setMode = function(mode, cb, resize) {
            if(resize==null) { resize = true; }
            mode = convertAceModeToMonacoLang(mode);
            var oldModel = ed.getModel();
            var oldValue = ed.getValue();
            var newModel

            if (oldModel) {
                var oldScrollTop = ed.getScrollTop();
                var oldScrollLeft = ed.getScrollLeft();
                var oldSelections = ed.getSelections();
                var oldPosition = ed.getPosition();
                oldValue = oldModel.getValue() || "";
                try {
                    if(!oldModel.isDisposed()) { oldModel.dispose(); }
                } catch (error) { }
                ed.setModel(null);
                newModel = monaco.editor.createModel((oldValue || ""), mode);
                ed.setModel(newModel);
                ed.setScrollTop(oldScrollTop, 1/* immediate */);
                ed.setScrollLeft(oldScrollLeft, 1/* immediate */);
                ed.setPosition(oldPosition);
                ed.setSelections(oldSelections);
            } else {
                newModel = monaco.editor.createModel((oldValue || ""), mode);
                ed.setModel(newModel);
            }
            if (cb && typeof cb == "function") {
                cb();
            }
            if(resize) {
                this.resize(); //cause a call to layout()
            }
        }

        ed.getRange = function getRange(){
            var r =  ed.getSelection();
            r.start = {
                row: r.selectionStartLineNumber-1,
                column: r.selectionStartColumn-1
            }
            r.end = {
                row: r.endLineNumber-1,
                column: r.endColumn-1
            }
            return r;
        }

        ed.selection.getRange = ed.getRange;

        ed.session.insert = function insert(position, text) {
            //ACE position is zero based, monaco range is 1 based
            var range = new monaco.Range(position.row+1, position.column+1, position.row+1, position.column+1);
            var op = { range: range, text: text, forceMoveMarkers: true };
            _executeEdits(this,[op]);
        }

        ed.setReadOnly = function setReadOnly(readOnly) {
            ed.updateOptions({ readOnly: readOnly })
        }

        ed.session.replace = function replace(range, text) {
            var op = { range: range, text: text, forceMoveMarkers: true };
            _executeEdits(this,[op]);
        }

        function _executeEdits(editor, edits, endCursorState) {
            var isReadOnly = editor.getOption(monaco.editor.EditorOptions.readOnly.id)
            if (isReadOnly) {
                editor.getModel().pushEditOperations(editor.getSelections(), edits, function() {
                    return endCursorState ? endCursorState : null;
                });
            } else {
                editor.executeEdits("editor", edits);
            }
        }

        ed.selectAll =  function selectAll() {
            const range = ed.getModel().getFullModelRange();
            ed.setSelection(range);
        }

        ed.clearSelection = function clearSelection() {
            ed.setPosition({column:1,lineNumber:1});
        }

        ed.getSelectedText = function getSelectedText() {
            return ed.getModel().getValueInRange(ed.getSelection());
        }

        ed.insertSnippet = function insertSnippet(s) {
            //https://github.com/microsoft/monaco-editor/issues/1112#issuecomment-429580604
            //no great way of triggering snippets!
            let contribution = ed.getContribution("snippetController2");
            contribution.insert(s);
        }

        ed.destroy = function destroy() {
            if(watchTimer) { clearInterval(watchTimer); }
            try {
                //dispose serverside addExtraLib disposible we added - this is the only way (https://github.com/microsoft/monaco-editor/issues/1002#issuecomment-564123586)
                if(Object.keys(loadedLibs.JS).length) {
                    let entries = Object.entries(loadedLibs.JS);
                    for (let i = 0; i < entries.length; i++) {
                        try {
                            const entry = entries[i];
                            let name = entry[0];
                            loadedLibs.JS[name].dispose();
                            loadedLibs.JS[name] = null;
                            delete loadedLibs.JS[name];
                        } catch (error) { }
                    }
                }
                if(Object.keys(loadedLibs.TS).length) {
                    let entries = Object.entries(loadedLibs.TS);
                    for (let i = 0; i < entries.length; i++) {
                        try {
                            const entry = entries[i];
                            let name = entry[0];
                            loadedLibs.TS[name].dispose();
                            loadedLibs.TS[name] = null;
                            delete loadedLibs.TS[name];
                        } catch (error) { }
                    }
                }
            } catch (error) { }
            try {
                var m = this.getModel();
                if(m && !m.isDisposed()) {
                    ed._initState = null;
                    m.dispose();
                }
                this.setModel(null);
            } catch (e) { }

            $(el).remove();
            $(toolbarRow).remove();
            ed.dispose();
        }

        ed.resize = function resize() {
            ed.layout();
        }
        ed.renderer.updateFull = ed.resize.bind(ed);//call resize on ed.renderer.updateFull

        ed.getSession = function getSession() {
            return ed;
        }

        ed.getLength = function getLength() {
            var _model = ed.getModel();
            if (_model !== null) {
                return _model.getLineCount();
            }
            return 0;
        }

        /**
         * Scroll vertically as necessary and reveal a line.
         * @param {Number} lineNumber
         * @param {ScrollType|Number} [scrollType] Immediate: = 1, Smooth: = 0
         */
         ed.scrollToLine = function scrollToLine(lineNumber, scrollType) {
            ed.revealLine(lineNumber, scrollType);
        }

        ed.moveCursorTo = function moveCursorTo(lineNumber, colNumber) {
            ed.setPosition({ lineNumber: lineNumber, column: colNumber });
        }

        // monaco.MarkerSeverity
        // 1: "Hint"
        // 2: "Info"
        // 4: "Warning"
        // 8: "Error"
        // Hint: 1
        // Info: 2
        // Warning: 4
        // Error: 8
        ed.getAnnotations = function getAnnotations() {
            let aceCompatibleMarkers;
            try {
                const _model = ed.getModel();
                if (_model !== null) {
                    const id = _model.getLanguageId(); // e.g.  javascript
                    const ra = _model.uri.authority; //   e.g.  model
                    const rp = _model.uri.path; //        e.g.  /18
                    const rs = _model.uri.scheme; //      e.g.  inmemory
                    const modelMarkers = monaco.editor.getModelMarkers(_model) || [];
                    const thisEditorsMarkers = modelMarkers.filter(function (marker) {
                        const _ra = marker.resource.authority; // e.g.  model
                        const _rp = marker.resource.path; //      e.g.  /18
                        const _rs = marker.resource.scheme; //    e.g.  inmemory
                        return marker.owner == id && _ra === ra && _rp === rp && _rs === rs;
                    })
                    aceCompatibleMarkers = thisEditorsMarkers.map(function (marker) {
                        return {
                            row: marker.startLineNumber, //ACE compatible
                            column: marker.startColumn, //future
                            endColumn: marker.endColumn, //future
                            endRow: marker.endLineNumber, //future
                            text: marker.message,//ACE compatible
                            type: monaco.MarkerSeverity[marker.severity] ? monaco.MarkerSeverity[marker.severity].toLowerCase() : marker.severity //ACE compatible
                        }
                    })
                }
            } catch (error) {
                console.log("Failed to get editor Annotations", error);
            }
            return aceCompatibleMarkers || [];
        }


        //ACE row and col are zero based.  Add 1 for monaco lineNumber and column
        ed.gotoLine = function gotoLine(row, col) {
            ed.setPosition({ lineNumber: row+1, column: col+1 });
        }
        //ACE row and col are zero based.  Minus 1 from monaco lineNumber and column
        ed.getCursorPosition = function getCursorPosition() {
            var p = ed.getPosition();
            return { row: p.lineNumber-1, column: p.column-1 };
        }

        ed.setTheme = function(theme) {
            monaco.editor.setTheme(theme);
            userSelectedTheme = theme;//remember users choice for this session
        }

        ed.on = function (name, cb) {
            switch (name) {
                case "change":
                case "input":
                    name = "onDidChangeModelContent";
                    break;
                case "focus":
                    name = "onDidFocusEditorWidget";
                    break;
                case "blur":
                    name = "onDidBlurEditorWidget";
                    break;
                case "paste":
                    name = "onDidPaste";
                    break;
                default:
                    break;
            }
            var fn;
            if (ed[name]) {
                fn = ed[name]
            } else if (monaco.editor[name]) {
                fn = monaco.editor[name];
            } else {
                console.warn("monaco - unknown event: " + name)
                return;
            }
            fn(cb);
        }
        ed.getUndoManager = function getUndoManager() {
            var o = {};
            function isClean() {
                try {
                    return ed.getModel().canUndo() === false
                } catch (error) {
                    return false
                }

            }
            o.isClean = isClean.bind(ed);
            return o;
        }
        ed.setFontSize = function setFontSize(size) {
            ed.updateOptions({ fontSize: size });
        }
        //#endregion "ACE compatability"

        //final setup
        ed.focusMemory = options.focus;
        ed._mode = editorOptions.language;

        //as models are signleton, consts and let are avialable to other javascript instances
        //so when not focused, set editor mode to text temporarily to avoid multiple defs

        if(editorOptions._language) {
            ed._mode = editorOptions._language;
            ed._tempMode = editorOptions.language;
        }

        ed.onDidBlurEditorWidget(function() {
            ed.focusMemory = false;
            ed.saveView();
            if(isVisible(el) == false) {
                onVisibilityChange(false, 0, el);
            }
        });
        ed.onDidFocusEditorWidget(function() {
            onVisibilityChange(true, 10, el);
        });

        function visibilityWatcher(element, callback) {
            try {
                var options = {
                    root: $(element).closest("div.red-ui-tray-content")[0] || document,
                    attributes: true,
                    childList: true,
                };
                var observer = new IntersectionObserver(function(entries, observer) {
                    entries.forEach(function(entry) {
                        callback(entry.intersectionRatio > 0, 5, entry.target);
                    });
                }, options);
                observer.observe(element);
            } catch (e1) {
                //browser not supporting IntersectionObserver? then fall back to polling!
                try {
                    let elVisibleMem = isVisible(el)
                    watchTimer = setInterval(function() {
                        let elVisible = isVisible(el);
                        if(elVisible != elVisibleMem) {
                            callback(elVisible, 5, element);
                        }
                        elVisibleMem = elVisible;
                    }, 100);
                } catch (e2) { }
            }
        }

        function onVisibilityChange(visible, delay, element) {
            delay = delay || 50;
            if (visible) {
                if (ed.focusMemory) {
                    setTimeout(function () {
                        if (element.parentElement) { //ensure el is still in DOM
                            ed.focus();
                        }
                    }, 300)
                }
                if (ed._initState) {
                    setTimeout(function () {
                        if (element.parentElement) { //ensure el is still in DOM
                            ed.restoreViewState(ed._initState);
                            ed._initState = null;
                        }
                    }, delay);
                }
                if (ed._mode == "javascript" && ed._tempMode == "text") {
                    ed._tempMode = "";
                    setTimeout(function () {
                        if (element.parentElement) { //ensure el is still in DOM
                            ed.setMode('javascript', undefined, false);
                        }
                    }, delay);
                }
            } else if (ed._mode == "javascript" && ed._tempMode != "text") {
                if (element.parentElement) { //ensure el is still in DOM
                    ed.setMode('text', undefined, false);
                    ed._tempMode = "text";
                }
            }
        }

        visibilityWatcher(el, onVisibilityChange);


        if (editorOptions.language === 'markdown') {
            $(el).addClass("red-ui-editor-text-container-toolbar");
            ed.toolbar = RED.editor.customEditTypes['_markdown'].buildToolbar(toolbarRow, ed);
            if (options.expandable !== false) {
                var expandButton = $('<button type="button" class="red-ui-button" style="float: right;"><i class="fa fa-expand"></i></button>').appendTo(ed.toolbar);
                RED.popover.tooltip(expandButton, RED._("markdownEditor.expand"));
                expandButton.on("click", function (e) {
                    e.preventDefault();
                    var value = ed.getValue();
                    ed.saveView();
                    RED.editor.editMarkdown({
                        value: value,
                        width: "Infinity",
                        stateId: options.stateId,
                        cancel: function () {
                            ed.focus();
                        },
                        complete: function (v, cursor) {
                            ed.setValue(v, -1);
                            setTimeout(function () {
                                ed.focus();
                                ed.restoreView();
                            }, 300);
                        }
                    })
                });
            }
            var helpButton = $('<button type="button" class="red-ui-editor-text-help red-ui-button red-ui-button-small"><i class="fa fa-question"></i></button>').appendTo($(el).parent());
            RED.popover.create({
                target: helpButton,
                trigger: 'click',
                size: "small",
                direction: "left",
                content: RED._("markdownEditor.format"),
                autoClose: 50
            });
        }
        ed.getView = function () {
            return ed.saveViewState();
        }
        ed.saveView = function (debuginfo) {
            if (!options.stateId) { return; } //only possible if created with a unique stateId
            window._editorStateMonaco = window._editorStateMonaco || {};
            var state = ed.getView();
            window._editorStateMonaco[options.stateId] = state;
            return state;
        }
        ed.restoreView = function (state) {
            if (!options.stateId) { return; } //only possible if created with a unique stateId
            window._editorStateMonaco = window._editorStateMonaco || {};
            var _state = state || window._editorStateMonaco[options.stateId];
            if (!_state) { return; } //no view state available
            try {
                if (ed.type) { //is editor already initialised?
                    ed.restoreViewState(_state);
                } else {
                    ed._initState = _state;
                }
            } catch (error) {
                delete window._editorStateMonaco[options.stateId];
            }
        };
        ed.restoreView();
        if (options.cursor && !ed._initState) {
            var row = options.cursor.row || options.cursor.lineNumber;
            var col = options.cursor.column || options.cursor.col;
            ed.gotoLine(row, col);
        }
        ed.type = type;

        ed.onKeyDown((event) => {
            if (event.keyCode === monaco.KeyCode.Escape) {
                if (monacoWidgetsAreOpen(ed)) {
                    event.preventDefault();
                }
            }
        })

        return ed;
    }

    function isVisible(el) {
        if(!el.offsetHeight && !el.offsetWidth) { return false; }
        if(getComputedStyle(el).visibility === 'hidden') { return false; }
        return true;
    }

    function monacoWidgetsAreOpen(editor) {
        /** @type {HTMLElement} */
        const editorDomNode = editor.getDomNode()
        const suggestVisible = !!editorDomNode.querySelector('.monaco-editor .suggest-widget.visible');
        const parameterHintsVisible = !!editorDomNode.querySelector('.monaco-editor .parameter-hints-widget.visible');
        const findWidgetVisible = !!editorDomNode.querySelector('.monaco-editor .find-widget.visible');
        const renameInputVisible = !!editorDomNode.querySelector('.monaco-editor .rename-box');
        return suggestVisible || parameterHintsVisible || findWidgetVisible || renameInputVisible
    }

    return {
        /**
         * Editor type
         * @memberof RED.editor.codeEditor.monaco
         */
         get type() { return type; },
         /**
          * Editor initialised
         * @memberof RED.editor.codeEditor.monaco
         */
        get initialised() { return initialised; },
        /**
         * Initialise code editor
         * @param {object} options - initialisation options
         * @memberof RED.editor.codeEditor.monaco
         */
        init: init,
        /**
         * Create a code editor
         * @param {object} options - the editor options
         * @memberof RED.editor.codeEditor.monaco
         */
        create: create
    }
})();
