AE.Console.Extensions.register({
  name: "ReloadScripts",
  description: "Keeps track of recently loaded scripts and adds them to the reload button.",
  enabled: true,
  content: function(exports) {
    var tmpPath = "";
    var tmpID = null;
    var checkboxes = {};
    var menuEntries = {};
    var menu_reload;

    var createMenuEntry = function(path, checked) {
      if (menu_reload) {
        var span = document.createElement("span");
        var label = document.createElement("label");
        var input = document.createElement("input");
        input.setAttribute("type", "checkbox");
        input.setAttribute("name", path);
        if (checked!==false) { input.setAttribute("checked", true); }
        input.path = path;
        AE.Events.bind(input, "click", toggleMenuEntry);
        label.appendChild(input);
        label.appendChild(document.createTextNode(path + " " + JSON.stringify(checked)));
        span.appendChild(label);
        // Button to remove menu entry.
        var closeButton = document.createElement("button");
        closeButton.appendChild(document.createTextNode("×"));
        closeButton.className = "button_remove right icon";
        closeButton.onclick = function() {
          removeMenuEntry(path);
          updateOptions();
          AE.Bridge.callRuby("stop_observe_file", path);
        };
        span.appendChild(closeButton);
        menu_reload.addItem(span);
        // Remember the item.
        menuEntries[path] = span;
        checkboxes[path] = input;
      }
    };

    var toggleMenuEntry = function() {
      updateOptions();
      if (this.checked) {
        AE.Bridge.callRuby("start_observe_file_changed", this.path);
      } else {
        AE.Bridge.callRuby("stop_observe_file", this.path);
      }
    };

    var removeMenuEntry = function(path) {
      var menu_reload = AE.Console.Menus.getMenu("menu_reload");
      var element = menuEntries[path];
      if (menu_reload && element) {
        menu_reload.removeItem(element);
        delete menuEntries[path];
        delete checkboxes[path];
      }
    };

    var updateOptions = function() {
      var scripts = {};
      for (path in checkboxes) {
        scripts[path] = checkboxes[path].checked;
      }
      AE.Bridge.updateOptions({reload_scripts: scripts});
    };

    var reload = function() {
      var scripts = [];
      for (path in checkboxes) {
        if (checkboxes[path].checked) { scripts.push(path); }
      }
      AE.Bridge.callRuby("reload", scripts);
    };

    exports.onLoad = function(Options) {
      if (!Options.reload_scripts) { Options.reload_scripts = {}; }
      menu_reload = AE.Console.Menus.getMenu("menu_reload");
      // Add all previous items.
      for (path in Options.reload_scripts) {
        createMenuEntry(path, Options.reload_scripts[path]);
      }
      // Add feature to button,
      AE.Events.bind(AE.$("#button_reload"), "click", function() {
        reload();
        AE.Console.editor.focus();
      });
    };

    // Test if a script is going to be loaded, then mark it to be added on success.
    exports.onInput = function(text, metadata) {
      if (/(?:load|require)[\(|\s][\"\'][^\)\"\']+\)?/.test(text)) {
        tmpID = metadata.id;
        tmpPath = text.match( /(?:load|require)[\(\s][\"\']([^\)\"\']+)[\"\']\)?/ )[1];
      };
    };

    // If the script was loaded successfully, then add it to the menu.
    exports.onResult = function(text, metadata) {
      if (metadata.source == tmpID) {
        // If it is not yet contained in the menu.
        if (!checkboxes[tmpPath]) {
          createMenuEntry(tmpPath, true);
          updateOptions();
          AE.Bridge.callRuby("start_observe_file_changed", tmpPath);
        };
      };
    };

    exports.update = function(hash) {
      var diff = {};
      for (path in menuEntries) { diff[path] = menuEntries[path]; }
      // Update paths from hash.
      for (path in hash) {
        delete diff[path];
        if (path in menuEntries) { // Exists already in this menu
          // Update the checked value
          checkboxes[path].checked = hash[path];
        } else { // Exists not in this menu
          createMenuEntry(path, hash[path]);
        }
      }
      // Remove all others.
      for (path in diff) {
        removeMenuEntry(path);
      }
    };
  }
});



AE.Console.Extensions.register({
  name: "InternalCommands",
  description: "Allows to define internal commands for the console.",
  enabled: true,
  content: function(exports) {
    var internalCommands = {};
    exports.onInput = function(text) {
      // Check for internal commands.
      if (text[0] === "\\" && text.split(/\n/).length <= 1) {
        var parts = text.slice(1).split(/\s+/);
        var command = parts.shift();
        for (var internalCommand in internalCommands) {
          if (internalCommand === command) {
            internalCommands[internalCommand].apply(window, parts);
            return "";
          };
        }
      }
    };
  }
});



AE.Console.Extensions.register({
  name: "History",
  description: "Allows to navigate back to previous input.",
  enabled: true,
  content: function(exports) {
    var history;

    // class HistoryProvider
    // This class allows to register strings and navigate back & forward.
    // This is only to provide the navigation capability on the JavaScript side.
    // A similar class on the Ruby side registers all input when it is evaled and
    // saves it to a file.
    var HistoryProvider = function() {
      // Same implementation in Ruby.
      var MAX_LENGTH = 100,
      selected = 0,
      editing  = "",
      data     = [];

      this.load = function(array) {
        if (Object.prototype.toString.call(array) !== '[object Array]') { return; }
        data = array;
      };

      this.back = function(current) {
        if (selected < -data.length) { return false; }
        if (selected === 0 && typeof current === "string") { editing = current; }
        selected -= 1;
        return data[data.length + selected];
      };

      this.forward = function() {
        if (selected >= 0) { return false; }
        selected += 1;
        if (selected === 0) { return editing; }
        return data[data.length + selected];
      };

      this.push = function(value) {
        selected = 0;
        if (value === data[data.length-1]) { return; }
        if (data.length > MAX_LENGTH) {
          data.shift();
        }
        data.push(value);
      };

    } // end class HistoryProvider

    exports.onLoad = function() {
      // Load the history.
      history = new HistoryProvider();
      AE.Bridge.callRuby("get_history", function(entries) {
        history.load(entries);
      });
    };

    exports.onArrowUp = function(editor) {
      var value = history.back(editor.getValue());
      if (typeof value === "string") {
        editor.setValue(value);
        editor.navigateFileStart();
        editor.navigateLineEnd();
      }
      editor.clearSelection();
    };

    exports.onArrowDown = function(editor) {
      var value = history.forward();
      if (typeof value === "string") {
        editor.setValue(value);
        editor.navigateFileEnd();
        editor.navigateLineEnd();
      }
      editor.clearSelection();
    };

    exports.onInput = function(text) {
      history.push(text);
    };
  }
});



AE.Console.Extensions.register({
  name: "DynamicToolbar",
  description: "A dynamic toolbar with useful features in the output field.",
  enabled: true,
  content: function(exports) {
    exports.onLoad = function() {
      // Add a CSS rule.
      AE.Style.setRule(".dynamic_toolbar", {
        display: "block",
        position: "absolute",
        "z-index": 1000,
        right: 0,
        "vertical-align": "middle"
      });
      AE.Style.setRule(".dynamic_toolbar button img", {
        width: "1em",
        height: "1em"
      });
    };

    exports.onCodeAdded = function(div, text, metadata) {
      var toolbar = document.createElement("div");
      toolbar.className = "dynamic_toolbar";
      toolbar.style.display = "none";
      toolbar.style.position = "absolute";
      toolbar.style.top = 0;
      toolbar.style.right = 0;
      // A button to repeat a command.
      if (metadata.type && /input/.test(metadata.type)) {
        var button_repeat = document.createElement("button");
        var icon_repeat = document.createElement("img");
        icon_repeat.src = "../images/icon_repeat.png";
        button_repeat.title = icon_repeat.alt = AE.Translate.get("Repeat");
        button_repeat.appendChild(icon_repeat);
        toolbar.appendChild(button_repeat);
        button_repeat.onclick = function() {
          AE.Console.editor.setValue(div.text);
          AE.Console.editor.clearSelection();
          AE.Console.editor.focus();
        }
        button_repeat.ondblclick = function() {
          AE.Console.editor.setValue(div.text);
          window.setTimeout(function(){
            AE.Console.editor.submit(div.text);
            AE.Console.editor.focus();
          }, 100);
        };
      };
      // A button to select an item.
      /*var button_select = document.createElement("button");
      var icon_select = document.createElement("img");
      icon_select.src = "../images/icon_select.png";
      button_select.title = icon_select.alt = AE.Translate.get("Select");
      button_select.appendChild(icon_select);
      toolbar.appendChild(button_select);
      button_select.onclick = function() {
        // TODO: Select the code.
      }*/
      // A button to remove an item from the output.
      var button_remove = document.createElement("button");
      var icon_remove = document.createElement("img");
      icon_remove.src = "../images/icon_remove.png";
      button_remove.title = icon_remove.alt = AE.Translate.get("Remove");
      button_remove.appendChild(icon_remove);
      toolbar.appendChild(button_remove);
      button_remove.onclick = function() {
        toolbar.parentNode.parentNode.removeChild(div);
        AE.Console.editor.focus();
      }
      //
      div.appendChild(toolbar);
      toolbar.timer = null;
      AE.Events.bind(div, "mouseover", function(){
        window.clearTimeout(toolbar.timer);
        toolbar.timer = window.setTimeout(function(){
          AE.Style.show(toolbar);
        }, 500);
      });
      AE.Events.bind(div, "mouseout", function(){
        window.clearTimeout(toolbar.timer);
        toolbar.timer = window.setTimeout(function(){
          AE.Style.hide(toolbar);
        }, 500);
      });
    };
  }
});



AE.Console.Extensions.register({
  name: "HighlightEntity",
  description: "This highlights SketchUp entities and points and colors when hovering the console.",
  enabled: true,
  content: function(exports) {
    exports.onLoad = function() {
      // Add a CSS rule.
      AE.Style.setRule(".highlight_entity", {
        border: "1px solid lightgray",
        display: "inline-block"
      });
      AE.Style.setRule(".highlight_entity:hover", {
        color: "Highlight",
        "border-color": "Highlight"
      });
    };
    exports.onBeforeHighlightedCodeAdded = function(text, metadata) {
      if (metadata.type && !/error|javascript/.test(metadata.type)) {
        return text
        // Add highlight feature to SketchUp entities and get their id (which is for some reason 2 * the Ruby object_id)
        .replace(/#(?:<|&lt;|&#60;)Sketchup\:\:(?:Face|Edge|Curve|ArcCurve|Image|Group|ComponentInstance|ComponentDefinition|Text|Drawingelement|ConstructionLine|ConstructionPoint|Vertex)\:([0-9abcdefx]+)(?:>|&gt;)/g, function(s, id_string){
            var id = parseInt(id_string) >> 1;
            return "<span class=\"highlight_entity\" id=\"" + id + "\" onmouseover=\"AE.Console.Extensions.HighlightEntity.entity(" + id + ")\" onmouseout=\"AE.Console.Extensions.HighlightEntity.stop(" + id + ")\">" + s + "</span>";
          })
        // Add highlight to Geom::BoudingBox
        .replace(/#(?:<|&lt;|&#60;)Geom\:\:(?:BoundingBox)\:([0-9abcdefx]+)(?:>|&gt;)/g, function(s, id_string){
            var id = parseInt(id_string) >> 1;
            return "<span class=\"highlight_entity\" id=\"" + id + "\" onmouseover=\"AE.Console.Extensions.HighlightEntity.entity(" + id + ")\" onmouseout=\"AE.Console.Extensions.HighlightEntity.stop(" + id + ")\">" + s + "</span>";
          })
        // Add highlight feature to Point3d
        .replace(/Point3d\(([0-9\.\-eE]+,[\s\u00a0]*[0-9\.\-eE]+,[\s\u00a0]*[0-9\.\-eE]+)\)/g, "<span class=\"highlight_entity\" onmouseover=\"AE.Console.Extensions.HighlightEntity.point([$1])\" onmouseout=\"AE.Console.Extensions.HighlightEntity.stop()\">Point3d($1)</span>")
        // Add highlight feature to Color
        .replace(/Color\(([\s\u00a0]*[0-9\.]+,[\s\u00a0]*[0-9\.]+,[\s\u00a0]*[0-9\.]+)(,[\s\u00a0]*[0-9\.]+)?\)/g, "<span class=\"highlight_entity\" onmouseover=\"this.style.backgroundColor='rgb($1)'\" onmouseout=\"this.style.backgroundColor=''\">Color($1$2)</span>");
      };
    };
    exports.entity = function(id) {
      AE.Bridge.callRuby("highlight_entity", id, function(valid) {
        // If the entity isn't valid (deleted or GC), remove the highlight feature.
        if (valid === false) {
          element = AE.$("#" + id);
          if (!element) { return; }
          AE.Style.removeClass(element, "highlight_entity");
          element.removeAttribute("onmouseover");
          element.removeAttribute("onmouseout");
        }
      });
    };
    exports.point = function(array){
      AE.Bridge.callRuby("highlight_point", array);
    };
    exports.stop = function() {
      AE.Bridge.callRuby("highlight_stop");
    };
  }
});



AE.Console.Extensions.register({
  name: "ReportModifierKeys",
  description: "Allows the SketchUp main window to see modifier keys pressed when the webdialog has focus.",
  enabled: true,
  content: function(exports) {
    exports.onLoad = function() {
      AE.Events.bind(document, "keydown", function(event){
        if (event.shiftKey || event.ctrlKey || event.altKey) {
          var keys = {
            shift: event.shiftKey,
            ctrl: event.ctrlKey,
            alt: event.altKey
          };
          AE.Bridge.callRuby("modifier_keys", keys);
        }
      });
      AE.Events.bind(document, "keyup", function(event){
        // Is the keycode of the key that triggered the event equal to shift, ctrl, alt?
        if (event.keyCode == 16 || event.keyCode == 17 || event.keyCode == 18) {
          // Then get the current status of the keys:
          var keys = {
            shift: event.shiftKey,
            ctrl: event.ctrlKey,
            alt: event.altKey
          };
          AE.Bridge.callRuby("modifier_keys", keys);
        }
      });
    };
  }
});



AE.Console.Extensions.register({
  name: "SelectTool",
  description: "This tool allows to conveniently get a reference by clicking an entity or point in the model.",
  enabled: true,
  content: function(exports) {
    exports.onLoad = function() {
      var button = AE.$("#button_select");
      AE.Events.bind(button, "click", function() {
        var selectionRange = AE.Console.editor.getSelectionRange();
        var selectedText = AE.Console.editor.session.getTextRange(selectionRange);
        AE.Bridge.callRuby("select_entity", selectedText, function(name){
          AE.Console.editor.getSession().getDocument().replace(selectionRange, name);
          window.focus();
          AE.Console.editor.focus();
        });
      });
    };
  }
});



AE.Console.Extensions.register({
  name: "ChangeTheme",
  description: "A theme selection option.",
  enabled: true,
  content: function(exports) {

    exports.onLoad = function(Options) {
      // Load the theme list with a callback to construct the HTMLSelectElement.
      ace.config.loadModule('ace/ext/themelist', function(themelist) {
        var themeSelectionItem = document.createElement("label");
        themeSelectionItem.appendChild( document.createTextNode( AE.Translate.get("Theme") + ":" ) );
        var select = document.createElement("select");
        select.name = "theme";
        select.size = 1;
        var brightGroup = document.createElement("optgroup");
        brightGroup.label = AE.Translate.get("Bright");
        var darkGroup = document.createElement("optgroup");
        darkGroup.label = AE.Translate.get("Dark");

        var item;
        for (var i = 0; i < themelist.themes.length; i++) {
          item = themelist.themes[i];
          option = document.createElement("option");
          option.appendChild( document.createTextNode(item.caption) );
          option.value = item.theme;
          if (Options.theme === item.theme) { option.selected = true; }
          if (item.isDark) {
            darkGroup.appendChild(option);
          } else {
            brightGroup.appendChild(option);
          }
        }
        select.appendChild(darkGroup);
        select.appendChild(brightGroup);
        themeSelectionItem.appendChild(select);
        AE.Console.Menus.getMenu("preferences").addItem(themeSelectionItem);

        // Preview on mouse hover.
        AE.Events.bind(select, "mouseover", function(e){
          this.desiredValue = e.target.value;
          if (!this.timer) { this.timer = window.setTimeout(this.updateTheme, 20); }
        });

        AE.Events.bind(select, "mouseout", function(e){
          this.desiredValue = null;
          if (!this.timer) { this.timer = window.setTimeout(this.updateTheme, 20); }
        });

        AE.Events.bind(select, "change", function(e){
          this.desiredValue = e.target.value;
          this.updateTheme();
          AE.Bridge.callRuby('update_options', { theme: select.value });
        });

        select.updateTheme = function(){
          var theme = select.desiredValue || select.value;
          try { AE.Console.editor.setTheme(theme); } catch(e) {}
          // Sync the ace editor theme CSS class with the content/output elements.
          window.setTimeout(function(){
            AE.$("#content").className = "ace-tm ace-" + theme.match(/[^\/]*$/)[0]; // [theme]
            AE.$("#output").className = AE.$("#input").className; // ace_editor [theme]
          }, 20);
          select.timer = null;
        };

        select.updateTheme();
      });
    };
  }
});



// Like: Detect when no changes to entities occur? Regexp search for Ruby API
// method names that modify entities.
AE.Console.Extensions.register({
  name: "FontSize",
  description: "Change the font size.",
  enabled: true,
  content: function(exports) {
    exports.onLoad = function(Options) {
      var item = document.createElement("label");
      var input = document.createElement("input");
      try{ input.type = "number"; } catch(e) { input.type = "text"; } // IE
      input.size = 3;
      input.name = "font_size";
      input.value = Options.font_size || AE.Console.editor.getFontSize();
      item.appendChild( document.createTextNode( AE.Translate.get("Font size") ) );
      item.appendChild(input);
      AE.Console.Menus.getMenu("preferences").addItem(item);
      AE.Events.bind(input, "change", function() {
        var fontSize = Number(input.value);
        if (fontSize > 5) {
          AE.Console.editor.setFontSize(fontSize);
          AE.$("#output").style.fontSize = fontSize + "px";
          AE.Bridge.callRuby('update_options', { font_size: fontSize });
        }
      });
      if (typeof Options.font_size === "number") {
        AE.Console.editor.setFontSize(Options.font_size);
        AE.$("#output").style.fontSize = Options.font_size + "px";
      }
    };
  }
});



// TODO: is it possible to detect when an operation is not needed?
// Like: Detect when no changes to entities occur? Regexp search for Ruby API
// method names that modify entities.
AE.Console.Extensions.register({
  name: "WrapInUndo",
  description: "Wrap every code execution into an undo operation. \
    This allows you to revert changes to SketchUp entities",
  enabled: true,
  content: function(exports) {
    exports.onLoad = function(Options) {
      var item = document.createElement("label");
      var checkbox = document.createElement("input");
      checkbox.type = "checkbox";
      checkbox.name = "wrap_in_undo";
      checkbox.checked = Options.wrap_in_undo || false;
      item.appendChild(checkbox);
      item.appendChild( document.createTextNode( AE.Translate.get("Wrap in undoable operation") ) );
      AE.Console.Menus.getMenu("preferences").addItem(item);

      AE.Events.bind(checkbox, "change", function() {
        AE.Bridge.callRuby('update_options', { wrap_in_undo: checkbox.checked });
      });
      AE.Events.bind(checkbox, "click", function() {
        AE.Bridge.callRuby('update_options', { wrap_in_undo: checkbox.checked });
      });
    };
  }
});



AE.Console.Extensions.register({
  name: "Autocompleter",
  description: "Suggests autocompletions.",
  enabled: true,
  content: function(exports) {
    // Matches complete reference name except of last . or ::
    //var contextRegExp = /((?:[a-zA-Z\.\_\:]*[a-zA-Z\_])?)(?:[\:]{1-2}|[\.\:])?$/;
    // Matches complete expression before prefix until whitespace:
    var contextRegExp = /(?:[^\s\(\)\[\]\{\}]*(?:\([^\(\)]*\)|\[[^\(\)]*\])?)?(?:\.|\:\:)?$/;

    var runtimeCompleter = {
      getCompletions: function(editor, session, pos, prefix, callback) {
        try {
          var line = editor.session.doc.getLine(pos.row);
          var context = line.slice(0, pos.column - prefix.length);
          context = context.match(contextRegExp)[0];
          // Query SketchUp for completions
          // Note that ace's autocompleters work with a chain of callbacks. If one
          // completer fails or does not fire its callback, there is no completion.
          AE.Bridge.callRuby("autocomplete", context, prefix, function(completions) {
            callback(null, completions);
          });
        } catch (e) { AE.Console.error("Error with autocompletion: "+e); }
      }
    };

    exports.onLoad = function(Options) {
      // Enable autocompletion
      ace.config.loadModule('ace/ext/language_tools', function(language_tools) {
        AE.Console.editor.setOptions({
          enableBasicAutocompletion: true,
          enableSnippets: true
        });
        // As of ace 1.1.2, the built-in completers are: (ext-language_tools line 69)
        // [snippetCompleter, textCompleter, keyWordCompleter]
        // They return too many irrelevant results, even worse due to the fuzzy filter,
        // so we want to keep only the snippetCompleter.
        AE.Console.editor.completers.pop();
        AE.Console.editor.completers.pop();
        // And we add a runtimeCompleter that queries completions from Ruby's introspection methods.
        language_tools.addCompleter(runtimeCompleter);
      });
    };
  }
});
