text: // Copyright (c) 2016, Matt Godbolt
  // All rights reserved.
  //
  // Redistribution and use in source and binary forms, with or without
  // modification, are permitted provided that the following conditions are met:
  //
  //     * Redistributions of source code must retain the above copyright notice,
  //       this list of conditions and the following disclaimer.
  //     * Redistributions in binary form must reproduce the above copyright
  //       notice, this list of conditions and the following disclaimer in the
  //       documentation and/or other materials provided with the distribution.
  //
  // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
  // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  // POSSIBILITY OF SUCH DAMAGE.
  "use strict";
  var _ = require('underscore');
  var $ = require('jquery');
  var colour = require('../colour');
  var loadSaveLib = require('../loadSave');
  var FontScale = require('../fontscale');
  var Components = require('../components');
  var monaco = require('monaco-editor');
  var options = require('../options');
  var Alert = require('../alert');
  var local = require('../local');
  var ga = require('../analytics');
  var monacoVim = require('monaco-vim');
  require('../modes/cppp-mode');
  require('../modes/d-mode');
  require('../modes/ispc-mode');
  require('../modes/llvm-ir-mode');
  require('../modes/haskell-mode');
  require('../modes/ocaml-mode');
  require('../modes/clean-mode');
  require('../modes/cuda-mode');
  require('../modes/fortran-mode');
  require('../modes/zig-mode');
  require('../modes/nc-mode');
  require('../modes/ada-mode');
  require('../modes/nim-mode');
  require('selectize');
  
  var loadSave = new loadSaveLib.LoadSave();
  var languages = options.languages;
  
  function Editor(hub, state, container) {
      this.id = state.id || hub.nextEditorId();
      this.container = container;
      this.domRoot = container.getElement();
      this.domRoot.html($('#codeEditor').html());
      this.hub = hub;
      this.eventHub = hub.createEventHub();
      // Should probably be its own function somewhere
      this.settings = JSON.parse(local.get('settings', '{}'));
      this.ourCompilers = {};
      this.ourExecutors = {};
      this.httpRoot = window.httpRoot;
      this.widgetsByCompiler = {};
      this.asmByCompiler = {};
      this.busyCompilers = {};
      this.colours = [];
  
      this.decorations = {};
      this.prevDecorations = [];
      this.extraDecorations = [];
  
      this.fadeTimeoutId = -1;
  
      this.editorSourceByLang = {};
      this.alertSystem = new Alert();
      this.alertSystem.prefixMessage = "Editor #" + this.id + ": ";
  
      this.awaitingInitialResults = false;
      this.selection = state.selection;
  
      this.langKeys = _.keys(languages);
      this.initLanguage(state);
  
      var root = this.domRoot.find(".monaco-placeholder");
      var legacyReadOnly = state.options && !!state.options.readOnly;
      this.editor = monaco.editor.create(root[0], {
          scrollBeyondLastLine: false,
          language: this.currentLanguage.monaco,
          fontFamily: this.settings.editorsFFont,
          readOnly: !!options.readOnly || legacyReadOnly,
          glyphMargin: !options.embedded,
          quickSuggestions: false,
          fixedOverflowWidgets: true,
          minimap: {
              maxColumn: 80
          },
          folding: true,
          lineNumbersMinChars: 1,
          emptySelectionClipboard: true,
          autoIndent: true,
          vimInUse: this.settings.useVim,
          fontLigatures: this.settings.editorsFLigatures
      });
      this.editor.getModel().setEOL(monaco.editor.EndOfLineSequence.LF);
  
      if (state.source !== undefined) {
          this.setSource(state.source);
      } else {
          this.updateEditorCode();
      }
  
      var startFolded = /^[/*#;]+\s*setup.*/;
      if (state.source && state.source.match(startFolded)) {
          // With reference to https://github.com/Microsoft/monaco-editor/issues/115
          // I tried that and it didn't work, but a delay of 500 seems to "be enough".
          setTimeout(_.bind(function () {
              this.editor.setSelection(new monaco.Selection(1, 1, 1, 1));
              this.editor.focus();
              this.editor.getAction("editor.fold").run();
              this.editor.clearSelection();
          }, this), 500);
      }
  
      this.initEditorActions();
      this.initButtons(state);
      this.initCallbacks();
  
      if (this.settings.useVim) {
          this.enableVim();
      }
  
      var usableLanguages = _.filter(languages, function (language) {
          return hub.compilerService.compilersByLang[language.id];
      });
  
      this.languageBtn.selectize({
          sortField: 'name',
          valueField: 'id',
          labelField: 'name',
          searchField: ['name'],
          options: _.map(usableLanguages, _.identity),
          items: [this.currentLanguage.id],
          dropdownParent: 'body'
      }).on('change', _.bind(function (e) {
          this.onLanguageChange($(e.target).val());
      }, this));
      this.selectize = this.languageBtn[0].selectize;
      // We suppress posting changes until the user has stopped typing by:
      // * Using _.debounce() to run emitChange on any key event or change
      //   only after a delay.
      // * Only actually triggering a change if the document text has changed from
      //   the previous emitted.
      this.lastChangeEmitted = null;
      this.onSettingsChange(this.settings);
      // this.editor.on("keydown", _.bind(function () {
      //     // Not strictly a change; but this suppresses changes until some time
      //     // after the last key down (be it an actual change or a just a cursor
      //     // movement etc).
      //     this.debouncedEmitChange();
      // }, this));
  
      this.updateTitle();
      this.updateState();
      ga.proxy('send', {
          hitType: 'event',
          eventCategory: 'OpenViewPane',
          eventAction: 'Editor'
      });
      ga.proxy('send', {
          hitType: 'event',
          eventCategory: 'LanguageChange',
          eventAction: this.currentLanguage.id
      });
  }
  
  Editor.prototype.onMotd = function (motd) {
      this.extraDecorations = motd.decorations;
      this.updateExtraDecorations();
  };
  
  Editor.prototype.updateExtraDecorations = function () {
      var decorationsDirty = false;
      _.each(this.extraDecorations, _.bind(function (decoration) {
          if (decoration.filter && decoration.filter.indexOf(this.currentLanguage.name.toLowerCase()) < 0) return;
          var match = this.editor.getModel().findNextMatch(decoration.regex, {
              column: 1,
              lineNumber: 1
          }, true, true, null, false);
          if (match !== this.decorations[decoration.name]) {
              decorationsDirty = true;
              this.decorations[decoration.name] = match ? [{range: match.range, options: decoration.decoration}] : null;
          }
      }, this));
      if (decorationsDirty)
          this.updateDecorations();
  };
  
  // If compilerId is undefined, every compiler will be pinged
  Editor.prototype.maybeEmitChange = function (force, compilerId) {
      var source = this.getSource();
      if (!force && source === this.lastChangeEmitted) return;
  
      this.updateExtraDecorations();
  
      this.lastChangeEmitted = source;
      this.eventHub.emit('editorChange', this.id, this.lastChangeEmitted, this.currentLanguage.id, compilerId);
  };
  
  Editor.prototype.updateState = function () {
      var state = {
          id: this.id,
          source: this.getSource(),
          lang: this.currentLanguage.id,
          selection: this.selection
      };
      this.fontScale.addState(state);
      this.container.setState(state);
  
      this.updateButtons();
  };
  
  Editor.prototype.setSource = function (newSource) {
      this.updateSource(newSource);
  };
  
  Editor.prototype.onNewSource = function (editorId, newSource) {
      if (this.id === editorId) {
          this.setSource(newSource);
      }
  };
  
  Editor.prototype.getSource = function () {
      return this.editor.getModel().getValue();
  };
  
  Editor.prototype.initLanguage = function (state) {
      this.currentLanguage = languages[this.langKeys[0]];
      this.waitingForLanguage = state.source && !state.lang;
      if (languages[this.settings.defaultLanguage]) {
          this.currentLanguage = languages[this.settings.defaultLanguage];
      }
      if (languages[state.lang]) {
          this.currentLanguage = languages[state.lang];
      } else if (this.settings.newEditorLastLang && languages[this.hub.lastOpenedLangId]) {
          this.currentLanguage = languages[this.hub.lastOpenedLangId];
      }
  };
  
  Editor.prototype.initCallbacks = function () {
      this.fontScale.on('change', _.bind(this.updateState, this));
  
      this.container.on('resize', this.resize, this);
      this.container.on('shown', this.resize, this);
      this.container.on('open', _.bind(function () {
          this.eventHub.emit('editorOpen', this.id);
      }, this));
      this.container.on('destroy', this.close, this);
      this.container.layoutManager.on('initialised', function () {
          // Once initialized, let everyone know what text we have.
          this.maybeEmitChange();
          // And maybe ask for a compilation (Will hit the cache most of the time)
          this.requestCompilation();
      }, this);
  
      this.eventHub.on('compilerOpen', this.onCompilerOpen, this);
      this.eventHub.on('executorOpen', this.onExecutorOpen, this);
      this.eventHub.on('compilerClose', this.onCompilerClose, this);
      this.eventHub.on('compiling', this.onCompiling, this);
      this.eventHub.on('compileResult', this.onCompileResponse, this);
      this.eventHub.on('selectLine', this.onSelectLine, this);
      this.eventHub.on('editorSetDecoration', this.onEditorSetDecoration, this);
      this.eventHub.on('editorLinkLine', this.onEditorLinkLine, this);
      this.eventHub.on('settingsChange', this.onSettingsChange, this);
      this.eventHub.on('conformanceViewOpen', this.onConformanceViewOpen, this);
      this.eventHub.on('conformanceViewClose', this.onConformanceViewClose, this);
      this.eventHub.on('resize', this.resize, this);
      this.eventHub.on('newSource', this.onNewSource, this);
      this.eventHub.on('motd', this.onMotd, this);
      this.eventHub.emit('requestMotd');
  
      this.editor.getModel().onDidChangeContent(_.bind(function () {
          this.debouncedEmitChange();
          this.updateState();
      }, this));
  
      this.mouseMoveThrottledFunction = _.throttle(_.bind(this.onMouseMove, this), 50);
  
      this.editor.onMouseMove(_.bind(function (e) {
          this.mouseMoveThrottledFunction(e);
      }, this));
  
      this.cursorSelectionThrottledFunction =
          _.throttle(_.bind(this.onDidChangeCursorSelection, this), 500);
      this.editor.onDidChangeCursorSelection(_.bind(function (e) {
          this.cursorSelectionThrottledFunction(e);
      }, this));
  
      this.eventHub.on('initialised', this.maybeEmitChange, this);
  
      $(document).on('keyup.editable', _.bind(function (e) {
          if (e.target === this.domRoot.find(".monaco-placeholder .inputarea")[0]) {
              if (e.which === 27) {
                  this.onEscapeKey(e);
              } else if (e.which === 45) {
                  this.onInsertKey(e);
              }
          }
      }, this));
  };
  
  Editor.prototype.onMouseMove = function (e) {
      if (e !== null && e.target !== null && this.settings.hoverShowSource && e.target.position !== null) {
          this.tryPanesLinkLine(e.target.position.lineNumber, false);
      }
  };
  
  Editor.prototype.onDidChangeCursorSelection = function (e) {
      if (this.awaitingInitialResults) {
          this.selection = e.selection;
          this.updateState();
      }
  };
  
  Editor.prototype.onEscapeKey = function () {
      if (this.editor.vimInUse) {
          var currentState = monacoVim.VimMode.Vim.maybeInitVimState_(this.vimMode);
          if (currentState.insertMode) {
              monacoVim.VimMode.Vim.exitInsertMode(this.vimMode);
          } else if (currentState.visualMode) {
              monacoVim.VimMode.Vim.exitVisualMode(this.vimMode, false);
          }
      }
  };
  
  Editor.prototype.onInsertKey = function (event) {
      if (this.editor.vimInUse) {
          var currentState = monacoVim.VimMode.Vim.maybeInitVimState_(this.vimMode);
          if (!currentState.insertMode) {
              var insertEvent = {};
              insertEvent.preventDefault = event.preventDefault;
              insertEvent.stopPropagation = event.stopPropagation;
              insertEvent.browserEvent = {};
              insertEvent.browserEvent.key = 'i';
              insertEvent.browserEvent.defaultPrevented = false;
              insertEvent.keyCode = 39;
              this.vimMode.handleKeyDown(insertEvent);
          }
      }
  };
  
  Editor.prototype.enableVim = function () {
      this.vimMode = monacoVim.initVimMode(this.editor, this.domRoot.find('#v-status')[0]);
      this.vimFlag.prop("class", "btn btn-info");
      this.editor.vimInUse = true;
  };
  
  Editor.prototype.disableVim = function () {
      this.vimMode.dispose();
      this.domRoot.find('#v-status').html("");
      this.vimFlag.prop("class", "btn btn-light");
      this.editor.vimInUse = false;
  };
  
  Editor.prototype.initButtons = function (state) {
      this.fontScale = new FontScale(this.domRoot, state, this.editor);
      this.languageBtn = this.domRoot.find('.change-language');
      // Ensure that the button is disabled if we don't have nothing to select
      // Note that is might be disabled for other reasons beforehand
      if (this.langKeys.length <= 1) {
          this.languageBtn.prop("disabled", true);
      }
      this.topBar = this.domRoot.find('.top-bar');
      this.hideable = this.domRoot.find('.hideable');
  
      this.loadSaveButton = this.domRoot.find('.load-save');
      var paneAdderDropdown = this.domRoot.find('.add-pane');
      var addCompilerButton = this.domRoot.find('.btn.add-compiler');
      var addExecutorButton = this.domRoot.find('.btn.add-executor');
      this.conformanceViewerButton = this.domRoot.find('.btn.conformance');
      var addEditorButton = this.domRoot.find('.btn.add-editor');
      var toggleVimButton = this.domRoot.find('#vim-flag');
      var togglePaneAdder = function () {
          paneAdderDropdown.dropdown('toggle');
      };
      this.vimFlag = this.domRoot.find('#vim-flag');
      toggleVimButton.on('click', _.bind(function () {
          if (this.editor.vimInUse) {
              this.disableVim();
          } else {
              this.enableVim();
          }
      }, this));
  
      // NB a new compilerConfig needs to be created every time; else the state is shared
      // between all compilers created this way. That leads to some nasty-to-find state
      // bugs e.g. https://github.com/mattgodbolt/compiler-explorer/issues/225
      var getCompilerConfig = _.bind(function () {
          return Components.getCompiler(this.id, this.currentLanguage.id);
      }, this);
  
      var getExecutorConfig = _.bind(function () {
          return Components.getExecutor(this.id, this.currentLanguage.id);
      }, this);
  
      var getConformanceConfig = _.bind(function () {
          return Components.getConformanceView(this.id, this.getSource(), this.currentLanguage.id);
      }, this);
  
      var getEditorConfig = _.bind(function () {
          return Components.getEditor();
      }, this);
  
      var addDragListener = _.bind(function (dragSource, dragConfig) {
          this.container.layoutManager
              .createDragSource(dragSource, dragConfig)
              ._dragListener.on('dragStart', togglePaneAdder);
      }, this);
  
      addDragListener(addCompilerButton, getCompilerConfig);
      addDragListener(addExecutorButton, getExecutorConfig);
      addDragListener(this.conformanceViewerButton, getConformanceConfig);
      addDragListener(addEditorButton, getEditorConfig);
  
      var bindClickEvent = _.bind(function (dragSource, dragConfig) {
          dragSource.click(_.bind(function () {
              var insertPoint = this.hub.findParentRowOrColumn(this.container) ||
                  this.container.layoutManager.root.contentItems[0];
              insertPoint.addChild(dragConfig);
          }, this));
      }, this);
  
      bindClickEvent(addCompilerButton, getCompilerConfig);
      bindClickEvent(addExecutorButton, getExecutorConfig);
      bindClickEvent(this.conformanceViewerButton, getConformanceConfig);
      bindClickEvent(addEditorButton, getEditorConfig);
  
      this.initLoadSaver();
      $(this.domRoot).keydown(_.bind(function (event) {
          if ((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 's') {
              event.preventDefault();
              if (this.settings.enableCtrlS) {
                  loadSave.setMinimalOptions(this.getSource(), this.currentLanguage);
                  if (!loadSave.onSaveToFile(this.id)) {
                      this.showLoadSaver();
                  }
              } else {
                  this.eventHub.emit("displaySharingPopover");
              }
          }
      }, this));
  
      this.cppInsightsButton = this.domRoot.find('.open-in-cppinsights');
      this.cppInsightsButton.on('mousedown', _.bind(function () {
          this.updateOpenInCppInsights();
      }, this));
  
      this.quickBenchButton = this.domRoot.find('.open-in-quickbench');
      this.quickBenchButton.on('mousedown', _.bind(function () {
          this.updateOpenInQuickBench();
      }, this));
  };
  
  Editor.prototype.updateButtons = function () {
      if (this.currentLanguage.id === 'c++') {
          this.cppInsightsButton.show();
          this.quickBenchButton.show();
      } else {
          this.cppInsightsButton.hide();
          this.quickBenchButton.hide();
      }
  };
  
  Editor.prototype.b64UTFEncode = function (str) {
      return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, v) {
          return String.fromCharCode(parseInt(v, 16));
      }));
  };
  
  Editor.prototype.asciiEncodeJsonText = function (json) {
      return json.replace(/[\u007F-\uFFFF]/g, function (chr) {
          // json unicode escapes must always be 4 characters long, so pad with leading zeros
          return "\\u" + ("0000" + chr.charCodeAt(0).toString(16)).substr(-4);
      });
  };
  
  Editor.prototype.getCompilerStates = function () {
      var states = [];
  
      _.each(this.ourCompilers, _.bind(function (val, compilerIdStr) {
          var compilerId = parseInt(compilerIdStr);
  
          var glCompiler = _.find(this.container.layoutManager.root.getComponentsByName("compiler"), function (c) {
              return c.id === compilerId;
          });
  
          if (glCompiler) {
              var state = glCompiler.currentState();
              states.push(state);
          }
      }, this));
  
      return states;
  };
  
  Editor.prototype.updateOpenInCppInsights = function () {
      var cppStd = 'cpp2a';
  
      var compilers = this.getCompilerStates();
      _.each(compilers, _.bind(function (compiler) {
          if ((compiler.options.indexOf("-std=c++11") !== -1) ||
              (compiler.options.indexOf("-std=gnu++11") !== -1)) {
              cppStd = "cpp11";
          } else if ((compiler.options.indexOf("-std=c++14") !== -1) ||
              (compiler.options.indexOf("-std=gnu++14") !== -1)) {
              cppStd = "cpp14";
          } else if ((compiler.options.indexOf("-std=c++17") !== -1) ||
              (compiler.options.indexOf("-std=gnu++17") !== -1)) {
              cppStd = "cpp17";
          } else if ((compiler.options.indexOf("-std=c++2a") !== -1) ||
              (compiler.options.indexOf("-std=gnu++2a") !== -1)) {
              cppStd = "cpp2a";
          } else if (compiler.options.indexOf("-std=c++98") !== -1) {
              cppStd = "cpp98";
          }
      }, this));
  
      var link = 'https://cppinsights.io/lnk?code=' + this.b64UTFEncode(this.getSource()) + '&std=' + cppStd + '&rev=1.0';
  
      this.domRoot.find(".open-in-cppinsights").attr("href", link);
  };
  
  Editor.prototype.cleanupSemVer = function (semver) {
      if (semver) {
          var semverStr = semver.toString();
          if ((semverStr !== "") && (semverStr.indexOf('(') === -1)) {
              var vercomps = semverStr.split('.');
              return vercomps[0] + '.' + (vercomps[1] ? vercomps[1] : '0');
          }
      }
  
      return false;
  };
  
  Editor.prototype.updateOpenInQuickBench = function () {
      var quickBenchState = {
          text: this.getSource()
      };
  
      var compilers = this.getCompilerStates();
  
      _.each(compilers, _.bind(function (compiler) {
          var knownCompiler = false;
  
          var compilerExtInfo = this.hub.compilerService.findCompiler(this.currentLanguage.id, compiler.compiler);
          var semver = this.cleanupSemVer(compilerExtInfo.semver);
          var groupOrName =
              compilerExtInfo.baseName ? compilerExtInfo.baseName :
                  compilerExtInfo.groupName ? compilerExtInfo.groupName : compilerExtInfo.name;
  
          if (semver && groupOrName) {
              groupOrName = groupOrName.toLowerCase();
              if (groupOrName.indexOf('gcc') !== -1) {
                  quickBenchState.compiler = "gcc-" + semver;
                  knownCompiler = true;
              } else if (groupOrName.indexOf('clang') !== -1) {
                  quickBenchState.compiler = "clang-" + semver;
                  knownCompiler = true;
              }
          }
  
          if (knownCompiler) {
              var match = compiler.options.match(/-(O([0-3sg]|fast))/);
              if (match !== null) {
                  if (match[2] === "fast") {
                      quickBenchState.optim = "F";
                  } else {
                      quickBenchState.optim = match[2].toUpperCase();
                  }
              }
  
              if ((compiler.options.indexOf("-std=c++11") !== -1) ||
                  (compiler.options.indexOf("-std=gnu++11") !== -1)) {
                  quickBenchState.cppVersion = "11";
              } else if ((compiler.options.indexOf("-std=c++14") !== -1) ||
                  (compiler.options.indexOf("-std=gnu++14") !== -1)) {
                  quickBenchState.cppVersion = "14";
              } else if ((compiler.options.indexOf("-std=c++17") !== -1) ||
                  (compiler.options.indexOf("-std=gnu++17") !== -1)) {
                  quickBenchState.cppVersion = "17";
              } else if ((compiler.options.indexOf("-std=c++2a") !== -1) ||
                  (compiler.options.indexOf("-std=gnu++2a") !== -1)) {
                  quickBenchState.cppVersion = "20";
              }
  
              if ((compiler.options.indexOf("-stdlib=libc++") !== -1)) {
                  quickBenchState.lib = "llvm";
              }
          }
      }, this));
  
      var link = 'http://quick-bench.com/#' + btoa(this.asciiEncodeJsonText(JSON.stringify(quickBenchState)));
      this.domRoot.find(".open-in-quickbench").attr("href", link);
  };
  
  Editor.prototype.changeLanguage = function (newLang) {
      this.selectize.setValue(newLang);
  };
  
  Editor.prototype.clearLinkedLine = function () {
      this.decorations.linkedCode = [];
      this.updateDecorations();
  };
  
  Editor.prototype.tryPanesLinkLine = function (thisLineNumber, reveal) {
      _.each(this.asmByCompiler, _.bind(function (asms, compilerId) {
          this.eventHub.emit('panesLinkLine', compilerId, thisLineNumber, reveal);
      }, this));
  };
  
  Editor.prototype.requestCompilation = function () {
      this.eventHub.emit('requestCompilation', this.id);
  };
  
  Editor.prototype.initEditorActions = function () {
      this.editor.addAction({
          id: 'compile',
          label: 'Compile',
          keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
          keybindingContext: null,
          contextMenuGroupId: 'navigation',
          contextMenuOrder: 1.5,
          run: _.bind(function () {
              // This change request is mostly superfluous
              this.maybeEmitChange();
              this.requestCompilation();
          }, this)
      });
  
      this.editor.addAction({
          id: 'toggleCompileOnChange',
          label: 'Toggle compile on change',
          keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Enter],
          keybindingContext: null,
          run: _.bind(function () {
              this.eventHub.emit('modifySettings', {
                  compileOnChange: !this.settings.compileOnChange
              });
              this.alertSystem
                  .notify('Compile on change has been toggled ' + (this.settings.compileOnChange ? 'ON' : 'OFF'), {
                      group: "togglecompile",
                      alertClass: this.settings.compileOnChange ? "notification-on" : "notification-off",
                      dismissTime: 3000
                  });
          }, this)
      });
  
      this.editor.addAction({
          id: 'clang-format',
          label: 'Format text',
          keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F9],
          keybindingContext: null,
          contextMenuGroupId: 'help',
          contextMenuOrder: 1.5,
          run: _.bind(this.formatCurrentText, this)
      });
  
      this.editor.addAction({
          id: 'toggleColourisation',
          label: 'Toggle colourisation',
          keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.F1],
          keybindingContext: null,
          run: _.bind(function () {
              this.eventHub.emit('modifySettings', {
                  colouriseAsm: !this.settings.colouriseAsm
              });
          }, this)
      });
  
      this.editor.addAction({
          id: 'viewasm',
          label: 'Reveal linked code',
          keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F10],
          keybindingContext: null,
          contextMenuGroupId: 'navigation',
          contextMenuOrder: 1.5,
          run: _.bind(function (ed) {
              this.tryPanesLinkLine(ed.getPosition().lineNumber, true);
          }, this)
      });
  };
  
  Editor.prototype.doesMatchEditor = function (otherSource) {
      return otherSource === this.getSource();
  };
  
  Editor.prototype.confirmOverwrite = function (yes) {
      this.alertSystem.ask("Changes were made to the code",
          "Changes were made to the code while it was being processed. Overwrite changes?",
          {yes: yes, no: null});
  };
  
  Editor.prototype.updateSource = function (newSource) {
      // Create something that looks like an edit operation for the whole text
      var operation = {
          range: this.editor.getModel().getFullModelRange(),
          forceMoveMarkers: true,
          text: newSource
      };
      var nullFn = function () {
          return null;
      };
      var viewState = this.editor.saveViewState();
      // Add a undo stop so we don't go back further than expected
      this.editor.pushUndoStop();
      // Apply de edit. Note that we lose cursor position, but I've not found a better alternative yet
      this.editor.getModel().pushEditOperations(viewState.cursorState, [operation], nullFn);
      this.numberUsedLines();
  
      if (!this.awaitingInitialResults) {
          if (this.selection) {
              this.editor.setSelection(this.selection);
              this.editor.revealLinesInCenter(this.selection.startLineNumber,
                  this.selection.endLineNumber);
          }
          this.awaitingInitialResults = true;
      }
  };
  
  Editor.prototype.formatCurrentText = function () {
      var previousSource = this.getSource();
  
      $.ajax({
          type: 'POST',
          url: window.location.origin + this.httpRoot + 'api/format/clangformat',
          dataType: 'json',  // Expected
          contentType: 'application/json',  // Sent
          data: JSON.stringify({
              source: previousSource,
              base: this.settings.formatBase
          }),
          success: _.bind(function (result) {
              if (result.exit === 0) {
                  if (this.doesMatchEditor(previousSource)) {
                      this.updateSource(result.answer);
                  } else {
                      this.confirmOverwrite(_.bind(function () {
                          this.updateSource(result.answer);
                      }, this), null);
                  }
              } else {
                  // Ops, the formatter itself failed!
                  this.alertSystem.notify("We encountered an error formatting your code: " + result.answer, {
                      group: "formatting",
                      alertClass: "notification-error"
                  });
              }
          }, this),
          error: _.bind(function (xhr, e_status, error) {
              // Hopefully we have not exploded!
              if (xhr.responseText) {
                  try {
                      var res = JSON.parse(xhr.responseText);
                      error = res.answer || error;
                  } catch (e) {
                      // continue regardless of error
                  }
              }
              error = error || "Unknown error";
              this.alertSystem.notify("We ran into some issues while formatting your code: " + error, {
                  group: "formatting",
                  alertClass: "notification-error"
              });
          }, this),
          cache: true
      });
  };
  
  Editor.prototype.resize = function () {
      var topBarHeight = this.updateAndCalcTopBarHeight();
  
      this.editor.layout({
          width: this.domRoot.width(),
          height: this.domRoot.height() - topBarHeight
      });
      // Only update the options if needed
      if (this.settings.wordWrap) {
          this.editor.updateOptions({
              wordWrapColumn: this.editor.getLayoutInfo().viewportColumn
          });
      }
  };
  
  Editor.prototype.updateAndCalcTopBarHeight = function () {
      var width = this.domRoot.width();
      if (width === this.cachedTopBarHeightAtWidth && !this.topBar.hasClass("d-none")) {
          return this.cachedTopBarHeight;
      }
  
      var topBarHeight = 0;
      var topBarHeightMax = 0;
      var topBarHeightMin = 0;
  
      if (!this.topBar.hasClass("d-none")) {
          this.hideable.show();
          topBarHeightMax = this.topBar.outerHeight(true);
          this.hideable.hide();
          topBarHeightMin = this.topBar.outerHeight(true);
          topBarHeight = topBarHeightMin;
          if (topBarHeightMin === topBarHeightMax) {
              this.hideable.show();
              topBarHeight = topBarHeightMax;
          }
      }
  
      this.cachedTopBarHeight = topBarHeight;
      this.cachedTopBarHeightAtWidth = width;
      return topBarHeight;
  };
  
  Editor.prototype.onSettingsChange = function (newSettings) {
      var before = this.settings;
      var after = newSettings;
      this.settings = _.clone(newSettings);
  
      this.editor.updateOptions({
          autoClosingBrackets: this.settings.autoCloseBrackets,
          useVim: this.settings.useVim,
          quickSuggestions: this.settings.showQuickSuggestions,
          contextmenu: this.settings.useCustomContextMenu,
          minimap: {
              enabled: this.settings.showMinimap && !options.embedded
          },
          fontFamily: this.settings.editorsFFont,
          fontLigatures: this.settings.editorsFLigatures,
          wordWrap: this.settings.wordWrap ? 'bounded' : 'off',
          wordWrapColumn: this.editor.getLayoutInfo().viewportColumn // Ensure the column count is up to date
      });
  
      // Unconditionally send editor changes. The compiler only compiles when needed
      this.debouncedEmitChange = _.debounce(_.bind(function () {
          this.maybeEmitChange();
      }, this), after.delayAfterChange);
  
      if (before.hoverShowSource && !after.hoverShowSource) {
          this.onEditorSetDecoration(this.id, -1, false);
      }
  
      if (after.useVim && !before.useVim) {
          this.enableVim();
      } else if (!after.useVim && before.useVim) {
          this.disableVim();
      }
  
      if (this.editor.getModel()) {
          this.editor.getModel().updateOptions({
              tabSize: this.settings.tabWidth,
              insertSpaces: this.settings.useSpaces
          });
      }
  
      this.numberUsedLines();
  };
  
  Editor.prototype.numberUsedLines = function () {
      var result = {};
      // First, note all lines used.
      _.each(this.asmByCompiler, function (asm) {
          _.each(asm, function (asmLine) {
              // If the line has a source indicator, and the source indicator is null (i.e. the
              // user's input file), then tag it as used.
              if (asmLine.source && asmLine.source.file === null && asmLine.source.line > 0)
                  result[asmLine.source.line - 1] = true;
          });
      });
      // Now assign an ordinal to each used line.
      var ordinal = 0;
      _.each(result, function (v, k) {
          result[k] = ordinal++;
      });
  
      if (_.any(this.busyCompilers)) return;
      this.updateColours(this.settings.colouriseAsm ? result : []);
  };
  
  Editor.prototype.updateColours = function (colours) {
      this.colours = colour.applyColours(this.editor, colours, this.settings.colourScheme, this.colours);
      this.eventHub.emit('colours', this.id, colours, this.settings.colourScheme);
  };
  
  Editor.prototype.onCompilerOpen = function (compilerId, editorId) {
      if (editorId === this.id) {
          // On any compiler open, rebroadcast our state in case they need to know it.
          if (this.waitingForLanguage) {
              var glCompiler = _.find(this.container.layoutManager.root.getComponentsByName("compiler"), function (c) {
                  return c.id === compilerId;
              });
              if (glCompiler) {
                  var selected = _.find(options.compilers, function (compiler) {
                      return compiler.id === glCompiler.originalCompilerId;
                  });
                  if (selected) {
                      this.changeLanguage(selected.lang);
                  }
              }
          }
          this.maybeEmitChange(true, compilerId);
          this.ourCompilers[compilerId] = true;
      }
  };
  
  Editor.prototype.onExecutorOpen = function (executorId, editorId) {
      if (editorId === this.id) {
          this.maybeEmitChange(true);
          this.ourExecutors[executorId] = true;
      }
  };
  
  Editor.prototype.onCompilerClose = function (compilerId) {
      if (this.ourCompilers[compilerId]) {
          monaco.editor.setModelMarkers(this.editor.getModel(), compilerId, []);
          delete this.widgetsByCompiler[compilerId];
          delete this.asmByCompiler[compilerId];
          delete this.busyCompilers[compilerId];
          delete this.ourCompilers[compilerId];
          this.numberUsedLines();
      }
  };
  
  Editor.prototype.onExecutorClose = function (id) {
      if (this.ourExecutors[id]) {
          delete this.ourExecutors[id];
      }
  };
  
  Editor.prototype.onCompiling = function (compilerId) {
      if (!this.ourCompilers[compilerId]) return;
      this.busyCompilers[compilerId] = true;
  };
  
  Editor.prototype.onCompileResponse = function (compilerId, compiler, result) {
      if (!this.ourCompilers[compilerId]) return;
      this.busyCompilers[compilerId] = false;
      var output = (result.stdout || []).concat(result.stderr || []);
      var widgets = _.compact(_.map(output, function (obj) {
          if (!obj.tag) return;
          var severity = 3; // error
          if (obj.tag.text.match(/^warning/)) severity = 2;
          if (obj.tag.text.match(/^note/)) severity = 1;
          return {
              severity: severity,
              message: obj.tag.text,
              source: compiler.name + ' #' + compilerId,
              startLineNumber: obj.tag.line,
              startColumn: obj.tag.column || 0,
              endLineNumber: obj.tag.line,
              endColumn: obj.tag.column ? -1 : Infinity
          };
      }, this));
      monaco.editor.setModelMarkers(this.editor.getModel(), compilerId, widgets);
      this.decorations.tags = _.map(widgets, function (tag) {
          return {
              range: new monaco.Range(tag.startLineNumber, tag.startColumn, tag.startLineNumber + 1, 1),
              options: {
                  isWholeLine: false,
                  inlineClassName: "error-code"
              }
          };
      }, this);
      this.updateDecorations();
      this.asmByCompiler[compilerId] = result.asm;
      this.numberUsedLines();
  };
  
  Editor.prototype.onSelectLine = function (id, lineNum) {
      if (Number(id) === this.id) {
          this.editor.setSelection({line: lineNum - 1, ch: 0}, {line: lineNum, ch: 0});
      }
  };
  
  Editor.prototype.onEditorLinkLine = function (editorId, lineNum, columnNum, reveal) {
      if (Number(editorId) === this.id) {
          if (reveal && lineNum) this.editor.revealLineInCenter(lineNum);
          this.decorations.linkedCode = lineNum === -1 || !lineNum ? [] : [{
              range: new monaco.Range(lineNum, 1, lineNum, 1),
              options: {
                  isWholeLine: true,
                  linesDecorationsClassName: 'linked-code-decoration-margin',
                  className: 'linked-code-decoration-line'
              }
          }];
  
          if (lineNum > 0 && columnNum !== -1) {
              this.decorations.linkedCode.push({
                  range: new monaco.Range(lineNum, columnNum, lineNum, columnNum + 1),
                  options: {
                      isWholeLine: false,
                      inlineClassName: 'linked-code-decoration-column'
                  }
              });
          }
  
          if (this.fadeTimeoutId !== -1) {
              clearTimeout(this.fadeTimeoutId);
          }
          this.fadeTimeoutId = setTimeout(_.bind(function () {
              this.clearLinkedLine();
              this.fadeTimeoutId = -1;
          }, this), 5000);
  
          this.updateDecorations();
      }
  };
  
  Editor.prototype.onEditorSetDecoration = function (id, lineNum, reveal) {
      if (Number(id) === this.id) {
          if (reveal && lineNum) this.editor.revealLineInCenter(lineNum);
          this.decorations.linkedCode = lineNum === -1 || !lineNum ? [] : [{
              range: new monaco.Range(lineNum, 1, lineNum, 1),
              options: {
                  isWholeLine: true,
                  linesDecorationsClassName: 'linked-code-decoration-margin',
                  inlineClassName: 'linked-code-decoration-inline'
              }
          }];
          this.updateDecorations();
      }
  };
  
  Editor.prototype.updateDecorations = function () {
      this.prevDecorations = this.editor.deltaDecorations(
          this.prevDecorations,
          _.compact(_.flatten(_.values(this.decorations))));
  };
  
  Editor.prototype.onConformanceViewOpen = function (editorId) {
      if (editorId === this.id) {
          this.conformanceViewerButton.attr("disabled", true);
      }
  };
  
  Editor.prototype.onConformanceViewClose = function (editorId) {
      if (editorId === this.id) {
          this.conformanceViewerButton.attr("disabled", false);
      }
  };
  
  Editor.prototype.showLoadSaver = function () {
      this.loadSaveButton.click();
  };
  
  Editor.prototype.initLoadSaver = function () {
      this.loadSaveButton
          .off('click')
          .click(_.bind(function () {
              loadSave.run(_.bind(function (text) {
                  this.setSource(text);
                  this.updateState();
                  this.maybeEmitChange(true);
                  this.requestCompilation();
              }, this), this.getSource(), this.currentLanguage);
          }, this));
  };
  
  Editor.prototype.onLanguageChange = function (newLangId) {
      if (languages[newLangId]) {
          if (newLangId !== this.currentLanguage.id) {
              var oldLangId = this.currentLanguage.id;
              this.currentLanguage = languages[newLangId];
              if (!this.waitingForLanguage && !this.settings.keepSourcesOnLangChange) {
                  this.editorSourceByLang[oldLangId] = this.getSource();
                  this.updateEditorCode();
              }
              this.initLoadSaver();
              monaco.editor.setModelLanguage(this.editor.getModel(), this.currentLanguage.monaco);
              this.updateTitle();
              this.updateState();
              // Broadcast the change to other panels
              this.eventHub.emit("languageChange", this.id, newLangId);
              this.maybeEmitChange(true);
              this.requestCompilation();
              ga.proxy('send', {
                  hitType: 'event',
                  eventCategory: 'LanguageChange',
                  eventAction: newLangId
              });
          }
          this.waitingForLanguage = false;
      }
  };
  
  Editor.prototype.getPaneName = function () {
      return this.currentLanguage.name + " source #" + this.id;
  };
  
  Editor.prototype.updateTitle = function () {
      this.container.setTitle(this.getPaneName());
  };
  
  // Called every time we change language, so we get the relevant code
  Editor.prototype.updateEditorCode = function () {
      this.setSource(this.editorSourceByLang[this.currentLanguage.id] || languages[this.currentLanguage.id].example);
  };
  
  Editor.prototype.close = function () {
      this.eventHub.unsubscribe();
      this.eventHub.emit('editorClose', this.id);
      this.editor.dispose();
  };
  
  module.exports = {
      Editor: Editor
  };
  