function Console(containerId) {
    this.counter = 0;
    this.containerId = containerId;
    this.inputId = containerId + "_expression";
    this.outputId = containerId + "_output";
    this.traces = [];
    this.buildView();
}

Console.prototype.escapeHTML = function (s) {
    var n = document.createElement("div");
    n.appendChild(document.createTextNode(s));
    return n.innerHTML;
}

Console.prototype.unescapeHTML = function (s) {
    var n = document.createElement("div");
    n.innerHTML = s;
    return n.firstChild.textContent;
}

Console.prototype.prependOutputDiv = function (styletext, content) {
    var d = document.createElement("div");
    d.style.cssText = styletext;
    d.innerHTML = content;

    var o = document.getElementById(this.outputId);
    o.insertBefore(d, o.firstChild);
}

Console.prototype.prependIO_HTML = function (styletext, input, outputHtml) {
    var id = this.containerId + 'expr' + this.counter;
    this.counter++;

    var editButtonId = id + '_editButton';
    var bodyHtml = '<div style="color: grey; font-style: italic">' +
    '<input style="float: right" id="'+editButtonId+'" value="Edit expression" type="submit">' +
    '<span id="'+id+'">' + this.escapeHTML(input.toString()) + '</span>' +
    '</div>';

    if (outputHtml != undefined) {
	this.prependOutputDiv(styletext, bodyHtml +
			      "<div style='border-top: 1px dashed grey'>" +
                              outputHtml +
			      "</div>");
    } else {
	this.prependOutputDiv(styletext, bodyHtml);
    }

    var thisConsole = this;
    document.getElementById(editButtonId)
    .addEventListener("click", function () { thisConsole.loadInput(id) }, false);
}

Console.prototype.shortDisplay = function (x) {
    var t =
      (x === null) ? "null"
    : (x instanceof Array) ? "array"
    : typeof(x);

    return "(" + t + ") " + this.escapeHTML((x === undefined || x === null) ? "" : x.toString());
}

Console.prototype.prependIO = function (styletext, input, output) {
    return this.prependIO_HTML(styletext, input,
                               output == undefined ? output : this.shortDisplay(output));
}

Console.prototype.recordOutput = function (input, output) {
    this.prependIO("border: 1px solid black; margin-top: 0.5em", input, output);
}

Console.prototype.inspectOutput = function (input, output) {
    var thisConsole = this;

    var values = [];
    var functions = [];
    var functionNames = [];
    var row;
    var ignoreValues = (typeof(output) == typeof(""));

    for (var k in output) {
        var v = output[k];
        row = "<tr><th width='100' style='text-align: right'>" + this.escapeHTML(k) + "</th><td>" +
            thisConsole.shortDisplay(v) +
            "</td></tr>";
        switch (typeof(v)) {
          case "function":
              functionNames.push(k);
              functions.push(row);
              break;
          default:
              if (!ignoreValues) {
                  values.push(row);
              }
              break;
        }
    }

    var outputHtml = "<table><tr><td colspan='2' style='border-bottom: 1px dashed grey'>" +
        thisConsole.shortDisplay(output) + "</td>";
    for (k = 0; k < values.length; k++) {
        outputHtml = outputHtml + values[k];
    }
    if (this.get_inspectFunctions()) {
        for (k = 0; k < functions.length; k++) {
            outputHtml = outputHtml + functions[k];
        }
    } else {
        var needComma = false;
        outputHtml = outputHtml + "<tr><td colspan='2' style='border-top: 1px dashed grey'>" +
            "function names: ";
        for (k = 0; k < functionNames.length; k++) {
            if (needComma) outputHtml = outputHtml + ", ";
            outputHtml = outputHtml + this.escapeHTML(functionNames[k]);
            needComma = true;
        }
        outputHtml = outputHtml + "</td></tr>";
    }
    outputHtml = outputHtml + "</table>";
    this.prependIO_HTML("border: 1px solid black; margin-top: 0.5em", input, outputHtml);
}

Console.prototype.recordError = function (input, e) {
    var style = "border: 2px solid red; margin-top: 0.5em";
    if ((typeof e == typeof {}) && ('message' in e)) {
	this.prependIO(style, input, e.message + " (" + e.name + ")");
    } else {
	this.prependIO(style, input, e.toString());
    }
}

Console.prototype.refocusInput = function () {
    var codearea = document.getElementById(this.inputId);
    codearea.select();
    codearea.focus();
}

Console.prototype.loadInput = function (id) {
    document.getElementById(this.inputId).value =
        this.unescapeHTML(document.getElementById(id).innerHTML);
    this.refocusInput();
}

Console.prototype.processInput = function (successContinuationName) {
    var codearea = document.getElementById(this.inputId);
    var code = codearea.value.replace(/^\s*|\s*$/g,'');
    if (code) {
	var result;
	var haveError = false;
	try {
            /* Greasemonkey operates in a strange no-mans-land with
             * regard to the Mozilla security model. If we say "eval"
             * here, we get the real (i.e. security-restricted) window
             * object as our global object, which doesn't let us see
             * properties (i.e. globals) defined by the page
             * itself. Instead, we pull the unsafeWindow (a
             * Greasemonkey-specific API) into scope using the icky
             * with statement (!). The unsafeWindow API provides a
             * non-security-restricted window into the page data, so
             * page-defined globals etc can be accessed. */
            var embeddedconsole_real_eval = eval;
            with (unsafeWindow) {
                result = embeddedconsole_real_eval(code);
            }
	} catch (e) {
	    haveError = true;
	    result = e;
	}
	if (haveError) {
	    this.recordError(code, result);
	} else {
	    this[successContinuationName](code, result);
	}
    }
    this.refocusInput();
}

Console.prototype.clearOutput = function () {
    document.getElementById(this.outputId).innerHTML = "";
    this.refocusInput();
}

Console.prototype.buildView = function () {
    var evalButtonId = this.containerId + "_evalButton";
    var inspectButtonId = this.containerId + "_inspectButton";
    var clearButtonId = this.containerId + "_clearButton";
    var inspectFunctionsId = this.containerId + "_inspectFunctions";

    var h = '<div style="margin-bottom: 0.5em">' +
    '<textarea id="'+this.inputId+'" rows="6" cols="70"></textarea><br>' +
    '<p>(Shortcut key: press ALT+SHIFT+P (on Windows or Linux) or CTRL+P (on Macintosh) to evaluate.</p>' +
    '<input id="'+evalButtonId+'" accesskey="p" type="submit" value="Evaluate">' +
    '<input id="'+inspectButtonId+'" accesskey="i" type="submit" value="Inspect">' +
    '<input id="'+clearButtonId+'" type="submit" value="Clear">' +
    '<input id="'+inspectFunctionsId+'" type="checkbox">Inspect functions' +
    '</div>' +
    '<div id="'+this.outputId+'"></div>';

    document.getElementById(this.containerId).innerHTML = h;

    this.get_inspectFunctions = function () {
        return document.getElementById(inspectFunctionsId).checked;
    }

    var thisConsole = this;
    document.getElementById(evalButtonId)
    .addEventListener("click", function () { thisConsole.processInput("recordOutput") }, false);
    document.getElementById(inspectButtonId)
    .addEventListener("click", function () { thisConsole.processInput("inspectOutput") }, false);
    document.getElementById(clearButtonId)
    .addEventListener("click", function () { thisConsole.clearOutput() }, false);
}

Console.prototype.hide = function () {
    document.getElementById(this.containerId).style.display = "none";
}

Console.prototype.show = function () {
    document.getElementById(this.containerId).style.display = "block";
}

Console.prototype.trace = function (key, scope) {
    var thisConsole = this;
    var originalFunction;
    var traceRecord;

    if (scope == null) {
        scope = unsafeWindow;
    }

    originalFunction = scope[key];
    if (!originalFunction) {
        return false;
    }

    traceRecord = {scope: scope,
                   key: key,
                   originalFunction: originalFunction,
                   lastInput: undefined,
                   lastOutput: undefined,
                   lastError: undefined};
    this.traces.push(traceRecord);
    scope[key] = function () {
        var args = [];
        for (var i = 0; i < arguments.length; i++) { args.push(arguments[i]); }
        thisConsole.inspectOutput(key + ": INPUT", args);
        traceRecord.lastInput = args;
        var result;
        try {
            result = originalFunction.apply(this, arguments);
        } catch (e) {
            thisConsole.recordError(key + ": ERROR", e);
            traceRecord.lastError = e;
            throw e;
        }
        thisConsole.inspectOutput(key + ": OUTPUT", result);
        traceRecord.lastOutput = result;
        return result;
    };

    return true;
}

Console.prototype.traceRecord = function (key, scope) {
    if (scope == null) {
        scope = unsafeWindow;
    }

    for (var i = 0; i < this.traces.length; i++) {
        var entry = this.traces[i];
        if (entry.scope === scope && entry.key === key) {
            return entry;
        }
    }
    return null;
}

Console.prototype.untrace = function (key, scope) {
    if (scope == null) {
        scope = unsafeWindow;
    }

    for (var i = 0; i < this.traces.length; i++) {
        var entry = this.traces[i];
        if (entry.scope === scope && entry.key === key) {
            scope[key] = entry.originalFunction;
            this.traces.splice(i, 1); // remove the entry
            return;
        }
    }
}

// ==UserScript==
// @name           Embedded Console
// @namespace      http://eighty-twenty.org/embedded-js-console
// @description    
// @include        *
// ==/UserScript==

var EmbeddedConsole = null;
function setupEmbeddedConsole() {
    var n = document.createElement("div");
    n.id = "__embeddedConsole__user__js__";
    document.body.appendChild(n);
    EmbeddedConsole = new Console("__embeddedConsole__user__js__");
}

setupEmbeddedConsole();

