// requires Base.js
var AE = window.AE || {};
/*
These lines in ace 1.1.2 needed to be patched to supress errors:
• ext-searchbox.js line 247 & 249
• ace.js line 2104 (Chromium): "this.textInput undefined"
• ace.js line 4474: "Could not complete the operation due to error 80020003."
• ace.js line 13744: "cssClass is null or not an object"
This line might need to be patched also:
• ext-searchbox.js line 252: "e = window.event"
This error might happend:
• ace.js (line 4339): Could not complete the operation due to error 80020003.
Apart from that, ace needs IE should run in the (latest) mode that is installed,
that means IE=edge.
*/
//AE.Bridge.callRuby=function(){console.log(JSON.stringify(Array.prototype.slice.call(arguments).slice(0)))}



AE.Console = function(self) {



// Object to hold options.
var Options = self.Options = {
  show_time: false,
  wrap_lines: false,
  verbosity: null,
  language: "ruby", // or "javascript" // Not yet implemented.
  theme: "ace/theme/chrome",
  binding: "global"
};



var editor, output, history,
  console_id = "js:",
  message_id = 0;



// Add input to the output.
// @param [String] text
// @param [Hash] metadata
self.input = function(text, metadata) {
  if (!metadata) { metadata = {}; }
  metadata.type = "input";
  Extensions.trigger("onInput", text, metadata);
  if (output) { output.add(text, metadata); }
}



// Add a return value to the output.
// @param [String] text
// @param [Hash] metadata
self.result = function(text, metadata) {
  if (!metadata) { metadata = {}; }
  metadata.type = "result";
  Extensions.trigger("onResult", text, metadata);
  if (output) { output.add(text, metadata); }
}



// Add an error to the output.
// @param [String] text
// @param [Hash] metadata
self.error = function(text, metadata) {
  if (!metadata) { metadata = {}; }
  metadata.type = "error";
  Extensions.trigger("onError", text, metadata);
  if (output) { output.add(text, metadata); }
}



// Add puts stdout to the output.
// @param [String] text
// @param [Hash] metadata
self.puts = function(text, metadata) {
  if (!metadata) { metadata = {}; }
  metadata.type = "puts";
  Extensions.trigger("onPuts", text, metadata);
  if (output) { output.add(text, metadata); }
}



// Add print stdout to the output.
// @param [String] text
// @param [Hash] metadata
self.print = function(text, metadata) {
  if (!metadata) { metadata = {}; }
  metadata.type = "print";
  Extensions.trigger("onPrint", text, metadata);
  if (output) { output.add(text, metadata); }
}



// Initialize the UI.
self.initialize = function(options) {
  // Load the options.
  if (!options) { options = {}; }
  for (var o in options) { Options[o] = options[o]; }
  if (Options.debug) { AE.debug = Options.debug; }

  // Set the console id that was assigned by the Ruby side.
  if (options.id) { console_id += options.id + ":"; }

  // Load defaults.
  AE.Form.fill(Options);

  /* INPUT FIELD */

  // Hack to hide the scrollbar.
  ace.require("ace/lib/dom").scrollbarWidth = function(document) { return 0; }

  // Create the ace editor.
  editor = self.editor = ace.edit("input");

  // Add default settings.
  try { editor.setTheme(Options.theme); } catch(e) {};
  editor.setDisplayIndentGuides(true);
  editor.setHighlightActiveLine(true);
  editor.setHighlightSelectedWord(true);
  editor.setSelectionStyle("line");
  editor.setShowInvisibles(true);
  editor.renderer.setShowGutter(true);
  editor.renderer.setShowPrintMargin(true);
  editor.session.setTabSize(2);
  editor.session.setUseSoftTabs(true);
  editor.session.setMode("ace/mode/ruby_sketchup");
  editor.session.setUseWrapMode(Options.wrap_lines);
  editor.session.setWrapLimitRange(null, null);
  editor.renderer.setPrintMarginColumn(80);
  editor.setOption("maxLines", 1000); // This autoresizes the editor.
  editor.setAutoScrollEditorIntoView(true); // Scrolls the editor into view on keyboard input.

  // Function to submit input for evaluation.
  editor.submit = function(text) {
    // Increase the message count.
    message_id++;
    // Add the command to the output.
    var metadata = { type: "input", language: Options.language, id: console_id+message_id }
    text = Extensions.trigger("onInput", text, metadata);
    output.add(text, metadata);
    // No need to eval empty commands.
    if ( text && !/^\s*$/.test(text) ) {
      // Send the command to ruby:
      if (Options.language === "ruby") {
        AE.Bridge.callRuby("eval", text, metadata);
      }
      // Or language is JavaScript and we eval it here:
      else {
        // We don't catch errors with try/catch here because that catches only manually thrown errors
        var result = JSON.stringify( globalEval(text) ) || "undefined";
        var metadata = {type: "result", language: "javascript", time: (new Date)};
        result = Extensions.trigger("onResult", result, metadata);
        output.add(result, metadata);
      }
    }
    // Clear the input field.
    editor.session.setValue("");
  };

  // Keyhandler to submit code for evaluation if Enter is pressed.
  editor.commands.addCommand({
    name: 'Evaluate code',
    bindKey: 'enter',
    exec: function(editor) {
      // Get the input.
      var command = editor.session.getValue();
      editor.submit(command);
      document.getElementById("input").scrollIntoView();
    },
    readOnly: true // false if this command should not apply in readOnly mode
  });

  // Keyhandler to override up/down navigation.
  editor.commands.addCommand({
    name: AE.Translate.get('Go one line up or back in the history.'),
    bindKey: 'up',
    exec: function(editor) {
      // If the cursor is in the first row.
      if (editor.getCursorPosition().row === 0) {
        Extensions.trigger("onArrowUp", editor);
      } else {
        editor.navigateUp();
      }
    }
  });

  // Keyhandler to override up/down navigation.
  editor.commands.addCommand({
    name: AE.Translate.get('Go one line down or forward in the history.'),
    bindKey: 'down',
    exec: function(editor) {
      // If the cursor is in the last row.
      if (editor.getCursorPosition().row + 1 === editor.session.getLength()) {
        Extensions.trigger("onArrowDown", editor);
      } else {
        editor.navigateDown();
      }
    }
  });

  /* OUTPUT FIELD */

  output = new Output(AE.$("#output"));

  AE.$("#output").className = AE.$("#input").className;

  AE.$("#content").className = "ace-tm";
  AE.$("#fake_gutter").className = "ace_gutter";

  /* TOOLBAR BUTTONS */

  // Clear
  AE.Events.bind(AE.$("#button_clear"), "click", function() {
    output.clear();
    editor.focus();
  });

  // Binding
  var binding_element = AE.$("#button_binding");
  AE.Events.bind(binding_element, "change", function() {
    var _this = this;
    var value = _this.value;
    // Invalid input
    if ( !/^(\$|@@?)?[^\!\"\'\`\@\$\%\|\&\/\(\)\[\]\{\}\,\;\?\<\>\=\+\-\*\/\#\~\\]+$/.test(value) ) {
      _this.value = "global";
      value = "global";
    }
    // Adjust the width.
    _this.style.width = (value.length/1.5) + "em"
    AE.Bridge.callRuby("set_binding", value, function(binding) {
      _this.value = binding;
      // Adjust the width.
      _this.style.width = (binding.length/1.5) + "em";
    });
  });
  binding_element.value = Options.binding;

  // Verbosity
  var verbosity_element = AE.$("[name=verbosity]")[0];
  AE.Events.bind(verbosity_element, "change", function() {
    if (!/^(true|false|null)$/.test(this.value)) { return; }
    Options.verbose = (/true/.test(this.value)) ? true : (/false/.test(this.value)) ? false : null;
    AE.Bridge.callRuby("set_verbose", Options.verbose);
  });
  verbosity_element.value = Options.verbose;

  // Wrap lines
  var wrap_lines_element = AE.$("[name=wrap_lines]")[0];
  AE.Events.bind(wrap_lines_element, "change", function() {
    Options.wrap_lines = this.checked;
    AE.Bridge.updateOptions({wrap_lines: Options.wrap_lines});
    this.update();
  });
  wrap_lines_element.update = function() {
    if (Options.wrap_lines) {
      AE.Style.addClass(AE.$("#content"), "wrap_lines");
    } else {
      AE.Style.removeClass(AE.$("#content"), "wrap_lines");
    }
    editor.session.setUseWrapMode(Options.wrap_lines);
  };
  wrap_lines_element.update();

  // Show time
  var show_time_element = AE.$("[name=show_time]")[0];
  AE.Events.bind(show_time_element, "change", function() {
    Options.show_time = this.checked;
    AE.Bridge.updateOptions({show_time: Options.show_time});
    this.update();
  });
  show_time_element.update = function() {
    AE.Style.setRule("#output .message .time", {
      display: (Options.show_time ? "block" : "none")
    });
  };
  show_time_element.update();

  // Initialize the menus.
  Menus.initialize();

  // Hook for loading.
  Extensions.trigger("onLoad", Options);

  // Focus the editor.
  AE.Events.bind(AE.$("#output"), "keydown", function(event) { if (!event.ctrlKey) { editor.focus(); } });
  AE.Events.bind(AE.$("#content"), "click", function(event) { if (event.target == this) { editor.focus(); } });
  window.setTimeout(function() { editor.focus(); }, 100);

};



// This function leverages ace's highlight capabilities outside of the editor.
// Here we turn insible editing characters off so that they won't pollute the
// produced html if someone wants to select & copy text.
var highlight = self.highlight = function(string) {
  if (typeof string !== "string" || string === "") { return ""; }
  var orig_showInvisibles = editor.renderer.getShowInvisibles();
  editor.renderer.setShowInvisibles(false);
  var html = [];
  var strings = string.split(/\r?\n/);
  for (var i=0; i<strings.length; i++) {
    if (strings[i] === "") { html.push("<br/>"); continue; } // TODO: Should this be "\n" or "<br/>"?
    var data = editor.session.getMode().getTokenizer().getLineTokens(strings[i], "start");
    editor.renderer.$textLayer.$renderSimpleLine(html, data.tokens);
    if (i+1 < strings.length) { html.push("<br/>"); } // TODO: Should this be "\n" or "<br/>"?
  }
  editor.renderer.setShowInvisibles(orig_showInvisibles);
  return html.join("");
};



// class Output
// This renders the html of the output.
var Output = self.Output = function(elem) {
  var element       = elem,
      createNewRow  = true,
      previousEntry = null;

  // Clear the item to the output
  this.clear = function() {
    element.innerHTML = "";
  };

  // Add an item to the output
  this.add = function (text, metadata) {
    if (typeof text !== "string") { text = String(text); }
    if (!metadata) { metadata = {}; }
    if (!metadata.type) { metadata.type = "other"; }
    metadata.language = metadata.language || Options.language;
    // Combine identical messages.
    if ((metadata.type === "puts" || metadata.type === "error") && previousEntry && text === previousEntry.text) {
      return previousEntry.counter.increase();
    }
    // Unfortunately SketchUp's stdout and stderr have only one `write` method
    // that prints inline text. To produce new lines, it sends a single line break
    // afterwards. In that case we absorb the line break and let the next message
    // start in a new line.
    if (text === "\n" && (metadata.type === "print" || metadata.type === "error")) { createNewRow = true; return; }
    if (metadata.type === "input" || metadata.type === "result") { createNewRow = true; }
    // Attach to the previous item.
    if (createNewRow === false && previousEntry) {
      attach(text);
    }
    // Or create a new item and a new row.
    else {
      newRow(text, metadata);
    }
    // Print messages are always attached to the previous entry.
    createNewRow = (metadata.type === "print") ? false : true;
  };

  // Insert text to the previous entry.
  var attach = function(text) {
    var span = document.createElement("code");
    span.innerHTML = highlight(text);
    previousEntry.appendChild(span);
    // Update the attribute.
    previousEntry.text += text;
  };

  // Create a new entry
  var newRow = function(text, metadata) {
    if (typeof text !== "string") {return; }
    var div = document.createElement("div");
    div.className = metadata.type + " message";
    div.className += " ace_scroller ace_text-layer"; // Tweaks for ace.

    // Add gutter on the left side to continue the editor's gutter and for message channel indicators.
    var gutter = document.createElement("div");
    gutter.className = "gutter";
    gutter.className += " ace-chrome ace_gutter"; // Tweaks for ace.
    div.appendChild(gutter);

    // Hook for raw text.
    text = Extensions.trigger("onBeforeCodeAdded", text, metadata);

    // Add the code.
    if (!/error/.test(metadata.type)) {
      var code = document.createElement("code");
      code.className = "content";
      div.text = text; // Save original text as attribute.
      var text = highlight(text);
      // Hook for highlighted text.
      text = Extensions.trigger("onBeforeHighlightedCodeAdded", text, metadata);
      code.innerHTML = text;
      div.appendChild(code);

    // Add Error details.
    } else {
      var panel = document.createElement("div");
      panel.className = "content ui-widget";
      var header = document.createElement("div");
      header.className = "ui-widget-header";
      header.appendChild(document.createTextNode(text));
      panel.appendChild(header);
      // Collapse long backtrace.
      if (metadata.backtrace) {
        AE.Style.addClass(panel, "collapsible-panel");
        var content = document.createElement("div");
        content.className = "ui-widget-content backtrace";
        content.appendChild(document.createTextNode( (metadata.backtrace).join("\n") ));
        panel.appendChild(content);
        AE.Style.hide(content);
        AE.Style.addClass(panel, "collapsed");
        var collapsed = true;
        AE.Events.bind(header, "click", function(){
          if (collapsed) {
            AE.Style.show(content);
            AE.Style.removeClass(panel, "collapsed");
          } else {
            AE.Style.hide(content);
            AE.Style.addClass(panel, "collapsed");
          }
          collapsed = !collapsed;
        });
      };
      div.appendChild(panel);
    }

    // Add counter to combine repeated entries.
    if (metadata.type === "puts" || metadata.type === "error") {
      div.counter = document.createElement("div");
      div.counter.className = "counter";
      div.appendChild(div.counter);
      div.counter.style.display = "none";
      div.counter.value = 1;
      div.counter.increase = function() {
        this.style.display = "block";
        this.value += 1;
        div.counter.innerHTML = this.value;
      };
    }

    // Add time stamp.
    // Metadata contains time in seconds, and JavaScript uses milliseconds.
    var d = (metadata.time instanceof Date) ? metadata.time : (typeof metadata.time === "number") ? new Date(metadata.time * 1000) : new Date;
    var time =  String(d.getHours()).rjust(2, "0") + ":" +
                String(d.getMinutes()).rjust(2, "0") + ":" +
                String(d.getSeconds()).rjust(2, "0") + "." +
                String(d.getMilliseconds()).rjust(3, "0");
    var timestamp = document.createElement("div");
    timestamp.className = "time";
    timestamp.innerHTML = time;
    div.appendChild(timestamp);
    element.appendChild(div);

    // Hook for div added to output.
    Extensions.trigger("onCodeAdded", div, text, metadata);
    previousEntry = code;
  };
}; // end class Output



// module Menus
var Menus = self.Menus = function(self) {
  // Initialize the menus.
  var menus = {};
  self.initialize = function() {
    var button_menus = AE.$(".button-menu");
    var menu_wrappers = [];
    for (var i=0; i<button_menus.length; i++) {
      (function(){
        var button = AE.$(".button", button_menus[i])[0];
        var menu_wrapper = AE.$(".menu-wrapper", button_menus[i])[0];
        var menu_content = AE.$(".menu", menu_wrapper)[0];
        menu_wrappers.push(menu_wrapper);
        menus[menu_content.getAttribute("name")] = menu_content;
        button.pressed = false;
        var activated = false;
        var pressed = false;
        // Method to show a menu.
        menu_wrapper.show = function() {
          AE.Style.show(menu_wrapper);
          activated = true;
        };
        // Method to hide a menu.
        menu_wrapper.hide = function() {
          AE.Style.hide(menu_wrapper);
          activated = false;
        };
        // Method to show a menu and hide all others.
        menu_wrapper.activate = function(event) {
          var a = !activated;
          for (var i=0; i<menu_wrappers.length; i++) {
            menu_wrappers[i].hide();
          }
          if (a) { menu_wrapper.show(menu_wrapper); }
          activated = a;
          if (event) { event.stopPropagation(); }
        }
        AE.Events.bind(button, "click", menu_wrapper.activate);
        // Prevent clicks in the menu from bubbling outside.
        AE.Events.bind(menu_wrapper, "click", function(event){
          event.stopPropagation();
        });
        // Method to add a new item to the menu.
        menu_content.addItem = function(string_or_element, fn) {
          var li = document.createElement("li");
          if (typeof string_or_element === "string") {
            li.innerHTML = string_or_element;
          } else {
            li.appendChild(string_or_element)
          }
          menu_content.appendChild(li);
          if (typeof fn === "function") {
            AE.Events.bind(li, "click", fn);
          }
        }
        // Method to remove an item from the menu.
        menu_content.removeItem = function(element) {
          menu_content.removeChild(element.parentNode);
        }
      })();
    };
    // A click outside the menu should close it.
    AE.Events.bind(document.body, "click", function(){
      for (var i=0; i<menu_wrappers.length; i++) {
        menu_wrappers[i].hide();
      }
    });
  };

  // Method to get a menu element.
  self.getMenu = function(name) {
    return menus[name];
  };

  return self;
}(self.Menus || {}); // end module Menus



// module Extensions
// This module allows other Console functions to provide hooks in which extensions can add/modify functionality.
var Extensions = self.Extensions = function(self) {
  var extensions = [];
  self.register = function(extension) {
    if (extension.enabled && typeof extension.content == "function") {
      extensions.push(extension);
      self[extension.name] = {};
      extension.content(self[extension.name]);
    }
  };

  // Problem: in JavaScript, strings are immutable. Because of that, modifications
  // to strings create a new string and don't modify the receiver like in Ruby.
  // So we need a mechanism to return a changed string. For simplicity we
  // do this only for the first argument.
  // When triggering an event and the main argument is a string that is
  // allowed to be modified, then we need to assign the return value to the same variable:
  // string = Extensions.trigger(event, string)
  self.trigger = function(event_name, data) {
    var args = Array.prototype.slice.call(arguments).slice(1);
    for (var i = 0; i < extensions.length; i++) {
      var extension = extensions[i];
      if (typeof self[extension.name][event_name] === "function") {
        try {
          var result = self[extension.name][event_name].apply(extension.content, args);
          // Change the main (first) argument.
          if (result !== args[0] && typeof result === typeof args[0]) { args[0] = result; }
        } catch(e) {
          AE.Console.error("Error in AE.Console.Extensions when triggering '"+event_name+
            "' on '"+extensions[i].name+"': "+e, {type: "error", language: "javascript"});
        }
      };
    };
    return args[0];
  };

  return self;
}(self.Extensions || {}); // end module Extensions



return self;
}(AE.Console || {}); // end module Console



// Catch errors in the WebDialog and send them to the console.
// There are some errors by the ACE editor (when double-clicking or selection
// that goes outside the editor) that would be silent in a normal Internet Explorer
// but cause popups in SketchUp.
window.onerror = function(errorMsg, url, lineNumber){
  AE.Console.error("JavaScript error:\n"+url+" (line "+lineNumber+"): "+errorMsg);
  return true;
}



if (typeof(String.prototype.repeat) === "undefined") {
  String.prototype.repeat = function(num) {
    for (var i=0, buf=""; i<num; i++) { buf += this; }
    return buf;
  }
}



if (typeof(String.prototype.rjust) === "undefined") {
  String.prototype.rjust = function(width, padding) {
    padding = padding || " ";
    padding = padding.substr( 0, 1 );
    if (this.length < width) {
      return padding.repeat(width - this.length) + this;
    } else {
      return this;
    }
  }
}
