// JAM Shell Wrapper Version 1.1.2

function winShell(opts) {
    opts = opts || {};
    var options = {
      cmd : null,
      name : "jam",
      nameopts : {length:8, memorable:true, lowercase:true},
      Nameopts : {length:8, memorable:true, uppercase:true},
      log : { Time: true, Network: false },
      output : printAnswer,
      server : true,
    }

    // http://www.squarefree.com/shell/shell.html
    var
        histList = [""],
        histPos = 0,
        _scope = {},
        _win, // a top-level context
        question,
        _in,
        _out,
        _scroll,
        tooManyMatches = null,
        lastError = null;

    function refocus() {
        if (!_in) return;
        _in.blur(); // Needed for Mozilla to scroll correctly.
        _in.focus();
    }
    
    function scrollToBottom(id){
      var div = document.getElementById(id);
      div.scrollTop = div.scrollHeight - div.clientHeight;
    }


    function setup () {
    }
    
    function init(input, output, scroll) {
        if (input) _in = document.getElementById(input);
        _out = document.getElementById(output);
        _scroll = scroll;
        _win = window;

        if (opener && !opener.closed) {
            console.log("Using bookmarklet version of shell: commands will run in opener's context.", "message");
            _win = opener;
        }

        initTarget();

        recalculateInputHeight();
        refocus();
        
        if (options.shell == undefined) {
          console.log('winshell: Creating jamsh ..') 
          options.shell = SHELL(options);
          options.cmd   = options.shell.init().cmd();
          options.name  = options.cmd.name('node');
        } else {
          // It is an agent monitor - attach to shell
          options.cmd   = options.shell.cmd();
          options.cmd.config({printAgent:printAnswer});
          options.name  = options.cmd.name('node');
        }
    }

    function initTarget() {
        _win.Shell = window;
        _win.print = shellCommands.print;
    }


    // Unless the user is selected something, refocus the textbox.
    // (requested by caillon, brendan, asa)
    function keepFocusInTextbox(e) {
        var g = e.srcElement ? e.srcElement : e.target; // IE vs. standard

        while (!g.tagName)
            g = g.parentNode;
        var t = g.tagName.toUpperCase();
        if (t == "A" || t == "INPUT")
            return;

        if (window.getSelection) {
            // Mozilla
            if (String(window.getSelection()))
                return;
        } else if (document.getSelection) {
            // Opera? Netscape 4?
            if (document.getSelection())
                return;
        } else {
            // IE
            if (document.selection.createRange().text)
                return;
        }

        refocus();
    }

    function inputKeydown(e) {
        // Use onkeydown because IE doesn't support onkeypress for arrow keys

        //alert(e.keyCode + " ^ " + e.keycode);

        if (e.shiftKey && e.keyCode == 13) { // shift-enter
            // don't do anything; allow the shift-enter to insert a line break as normal
        } else if (e.keyCode == 13) { // enter
            // execute the input on enter
            try {
                go();
            } catch (er) {
                alert(er);
            };
            setTimeout(function() {
                _in.value = "";
            }, 0); // can't preventDefault on input, so clear it later
        } else if (e.keyCode == 38) { // up
            // go up in history if at top or ctrl-up
            if (e.ctrlKey || caretInFirstLine(_in))
                hist(true);
        } else if (e.keyCode == 40) { // down
            // go down in history if at end or ctrl-down
            if (e.ctrlKey || caretInLastLine(_in))
                hist(false);
        } else if (e.keyCode == 9) { // tab
            tabcomplete();
            setTimeout(function() {
                refocus();
            }, 0); // refocus because tab was hit
        } else {}

        setTimeout(recalculateInputHeight, 0);

        //return true;
    };

    function caretInFirstLine(textbox) {
        // IE doesn't support selectionStart/selectionEnd
        if (textbox.selectionStart == undefined)
            return true;

        var firstLineBreak = textbox.value.indexOf("\n");

        return ((firstLineBreak == -1) || (textbox.selectionStart <= firstLineBreak));
    }

    function caretInLastLine(textbox) {
        // IE doesn't support selectionStart/selectionEnd
        if (textbox.selectionEnd == undefined)
            return true;

        var lastLineBreak = textbox.value.lastIndexOf("\n");

        return (textbox.selectionEnd > lastLineBreak);
    }

    function recalculateInputHeight() {
        if (!_in) return;
        var rows = _in.value.split(/\n/).length +
            1 // prevent scrollbar flickering in Mozilla
            +
            (window.opera ? 1 : 0); // leave room for scrollbar in Opera

        if (_in.rows != rows) // without this check, it is impossible to select text in Opera 7.60 or Opera 8.0.
            _in.rows = rows;
    }

    function loadScript(url, callback) {

        var script = document.createElement("script")
        script.type = "text/javascript";

        if (script.readyState) { //IE
            script.onreadystatechange = function() {
                if (script.readyState == "loaded" ||
                    script.readyState == "complete") {
                    script.onreadystatechange = null;
                    callback();
                }
            };
        } else { //Others
            script.onload = function() {
                callback();
            };
        }

        script.src = url;
        document.getElementsByTagName("head")[0].appendChild(script);
    }

    function get(url, callback) {
        // read text from URL location
        var request = new XMLHttpRequest();

        request.open('GET', url, true);
        request.send(null);
        request.onreadystatechange = function() {
            if (request.status==0) {
              println('No connection to LUAOS.');
              if (_listener) clearInterval(_listener);
            } else if (request.readyState === 4) {
                var type = request.getResponseHeader('Content-Type');
                if (type && type.indexOf("text") !== 1) {
                    callback(request.responseText);
                }
            }
        }
    }

    function put(url, data, callback) {
        // write text to URL location
        var request = new XMLHttpRequest();
        request.open('POST', url, true);
        request.send(data);
        request.onreadystatechange = function(err) {
            if (request.status==0) {
              println('No connection to LUAOS.');
              if (_listener) clearInterval(_listener);
            } else if (request.readyState === 4 && request.status === 200) {
                callback(request.responseText);
            }
        }
    }

    function println(s, type, time) {
        if ((s = String(s))) {
            var newdiv = document.createElement("div");
            newdiv.innerHTML=
              (time?'<span style="color:green">'+SHELL.Io.Time()+'</span> ':'')+
              s.replace(/&/g,'&amp;')
               .replace(/\t/g,'    ')
               .replace(/\s/g,'&nbsp;')
               .replace(/</g,'&lt;')
               .replace(/>/g,'&gt;');
            newdiv.className = type;
            _out.appendChild(newdiv);
            if (_scroll && _scroll.scrollAuto) {
              var scPos = _scroll.getScrollState();
              _scroll.scrollTo(0,scPos.y+500);
            }
            return newdiv;
        }
    }

    function printWithRunin(h, s, type) {
        var div = println(s, type);
        var head = document.createElement("strong");
        head.appendChild(document.createTextNode(h + ": "));
        div.insertBefore(head, div.firstChild);
    }

    function iter(a, func) {
        for (var p in a) func(a[p])
    }


    var shellCommands = {
        load: function load(url) {
            var s = _win.document.createElement("script");
            s.type = "text/javascript";
            s.src = url;
            _win.document.getElementsByTagName("head")[0].appendChild(s);
            println("Loading " + url + "...", "message");
        },

        clear: function clear() {
            var CHILDREN_TO_PRESERVE = 0;
            while (_out.childNodes[CHILDREN_TO_PRESERVE])
                _out.removeChild(_out.childNodes[CHILDREN_TO_PRESERVE]);
        },

        code: function code(index) {
            if (location.href.split('?').length == 1) return;
            var params = location.href.split('?')[1].split('&');
            var data = {},
                i = 1,
                file;

            for (var x in params) {
                data[params[x].split('=')[0]] = params[x].split('=')[1];
                if (i == index) file = data[params[x].split('=')[0]];
                i++;
            }
            if (file) {
                get('code/' + file, function(text) {
                    go(text)
                });
            }
        },

        codes: function codes() {
            if (location.href.split('?').length == 1) return;
            var params = location.href.split('?')[1].split('&');
            var data = {};

            for (var x in params) {
                data[params[x].split('=')[0]] = params[x].split('=')[1];
            }
            return data;
        },

        getResult: function getResult() {
            get(URL, function(reply) {
                printAnswer(reply);
            });
        },

        help: function help(topic) {


            printWithRunin("Features", "autocompletion of property names with Tab,");
            printWithRunin("Features", "multiline input with Shift+Enter, input history with (Ctrl+) Up/Down");

        },

        print: function print(s) {
            println(s, "print");
        },

        reset: function reset() {
            put(URL, ':reset');
        },

        // the normal function, "print", shouldn't return a value
        // (suggested by brendan; later noticed it was a problem when showing others)
        pr: function pr(s) {
            shellCommands.print(s); // need to specify shellCommands so it doesn't try window.print()!
            return s;
        },

        props: function props(e, onePerLine) {
            if (e === null) {
                println("props called with null argument", "error");
                return;
            }

            if (e === undefined) {
                println("props called with undefined argument", "error");
                return;
            }

            var ns = ["Methods", "Fields", "Unreachables"];
            var as = [
                [],
                [],
                []
            ]; // array of (empty) arrays of arrays!
            var p, j, i; // loop variables, several used multiple times

            var protoLevels = 0;

            for (p = e; p; p = p.__proto__) {
                for (i = 0; i < ns.length; ++i)
                    as[i][protoLevels] = [];
                ++protoLevels;
            }

            for (var a in e) {
                // Shortcoming: doesn't check that VALUES are the same in object and prototype.

                var protoLevel = -1;
                try {
                    for (p = e; p && (a in p); p = p.__proto__)
                        ++protoLevel;
                } catch (er) {
                    protoLevel = 0;
                } // "in" operator throws when param to props() is a string

                var type = 1;
                try {
                    if ((typeof e[a]) == "function")
                        type = 0;
                } catch (er) {
                    type = 2;
                }

                as[type][protoLevel].push(a);
            }

            function times(s, n) {
                return n ? s + times(s, n - 1) : "";
            }

            for (j = 0; j < protoLevels; ++j)
                for (i = 0; i < ns.length; ++i)
                    if (as[i][j].length)
                        printWithRunin(
                            ns[i] + times(" of prototype", j),
                            (onePerLine ? "\n\n" : "") + as[i][j].sort().join(onePerLine ? "\n" : ", ") + (onePerLine ? "\n\n" : ""),
                            "propList"
                        );
        },

        blink: function blink(node) {
            if (!node) throw ("blink: argument is null or undefined.");
            if (node.nodeType == null) throw ("blink: argument must be a node.");
            if (node.nodeType == 3) throw ("blink: argument must not be a text node");
            if (node.documentElement) throw ("blink: argument must not be the document object");

            function setOutline(o) {
                return function() {
                    if (node.style.outline != node.style.bogusProperty) {
                        // browser supports outline (Firefox 1.1 and newer, CSS3, Opera 8).
                        node.style.outline = o;
                    } else if (node.style.MozOutline != node.style.bogusProperty) {
                        // browser supports MozOutline (Firefox 1.0.x and older)
                        node.style.MozOutline = o;
                    } else {
                        // browser only supports border (IE). border is a fallback because it moves things around.
                        node.style.border = o;
                    }
                }
            }

            function focusIt(a) {
                return function() {
                    a.focus();
                }
            }

            if (node.ownerDocument) {
                var windowToFocusNow = (node.ownerDocument.defaultView || node.ownerDocument.parentWindow); // Moz vs. IE
                if (windowToFocusNow)
                    setTimeout(focusIt(windowToFocusNow.top), 0);
            }

            for (var i = 1; i < 7; ++i)
                setTimeout(setOutline((i % 2) ? '3px solid red' : 'none'), i * 100);

            setTimeout(focusIt(window), 800);
            if (_in) setTimeout(focusIt(_in), 810);
        },


        ans: undefined
    };


    function hist(up) {
        // histList[0] = first command entered, [1] = second, etc.
        // type something, press up --> thing typed is now in "limbo"
        // (last item in histList) and should be reachable by pressing
        // down again.

        var L = histList.length;

        if (L == 1)
            return;

        if (up) {
            if (histPos == L - 1) {
                // Save this entry in case the user hits the down key.
                histList[histPos] = _in.value;
            }

            if (histPos > 0) {
                histPos--;
                // Use a timeout to prevent up from moving cursor within new text
                // Set to nothing first for the same reason
                setTimeout(
                    function() {
                        _in.value = '';
                        _in.value = histList[histPos];
                        var caretPos = _in.value.length;
                        if (_in.setSelectionRange)
                            _in.setSelectionRange(caretPos, caretPos);
                    },
                    0
                );
            }
        } else // down
        {
            if (histPos < L - 1) {
                histPos++;
                _in.value = histList[histPos];
            } else if (histPos == L - 1) {
                // Already on the current entry: clear but save
                if (_in.value) {
                    histList[histPos] = _in.value;
                    ++histPos;
                    _in.value = "";
                }
            }
        }
    }

    function tabcomplete() {
        /*
         * Working backwards from s[from], find the spot
         * where this expression starts.  It will scan
         * until it hits a mismatched ( or a space,
         * but it skips over quoted strings.
         * If stopAtDot is true, stop at a '.'
         */
        function findbeginning(s, from, stopAtDot) {
            /*
             *  Complicated function.
             *
             *  Return true if s[i] == q BUT ONLY IF
             *  s[i-1] is not a backslash.
             */
            function equalButNotEscaped(s, i, q) {
                if (s.charAt(i) != q) // not equal go no further
                    return false;

                if (i == 0) // beginning of string
                    return true;

                if (s.charAt(i - 1) == '\\') // escaped?
                    return false;

                return true;
            }

            var nparens = 0;
            var i;
            for (i = from; i >= 0; i--) {
                if (s.charAt(i) == ' ')
                    break;

                if (stopAtDot && s.charAt(i) == '.')
                    break;

                if (s.charAt(i) == ')')
                    nparens++;
                else if (s.charAt(i) == '(')
                    nparens--;

                if (nparens < 0)
                    break;

                // skip quoted strings
                if (s.charAt(i) == '\'' || s.charAt(i) == '\"') {
                    //dump("skipping quoted chars: ");
                    var quot = s.charAt(i);
                    i--;
                    while (i >= 0 && !equalButNotEscaped(s, i, quot)) {
                        //dump(s.charAt(i));
                        i--;
                    }
                    //dump("\n");
                }
            }
            return i;
        }

        // XXX should be used more consistently (instead of using selectionStart/selectionEnd throughout code)
        // XXX doesn't work in IE, even though it contains IE-specific code
        function getcaretpos(inp) {
            if (inp.selectionEnd != null)
                return inp.selectionEnd;

            if (inp.createTextRange) {
                var docrange = _win.Shell.document.selection.createRange();
                var inprange = inp.createTextRange();
                if (inprange.setEndPoint) {
                    inprange.setEndPoint('EndToStart', docrange);
                    return inprange.text.length;
                }
            }

            return inp.value.length; // sucks, punt
        }

        function setselectionto(inp, pos) {
            if (inp.selectionStart) {
                inp.selectionStart = inp.selectionEnd = pos;
            } else if (inp.createTextRange) {
                var docrange = _win.Shell.document.selection.createRange();
                var inprange = inp.createTextRange();
                inprange.move('character', pos);
                inprange.select();
            } else { // err...
                /*
                  inp.select();
                  if(_win.Shell.document.getSelection())
                    _win.Shell.document.getSelection() = "";
                    */
            }
        }
        // get position of cursor within the input box
        var caret = getcaretpos(_in);

        if (caret) {
            //dump("----\n");
            var dotpos, spacepos, complete, obj;
            //dump("caret pos: " + caret + "\n");
            // see if there's a dot before here
            dotpos = findbeginning(_in.value, caret - 1, true);
            //dump("dot pos: " + dotpos + "\n");
            if (dotpos == -1 || _in.value.charAt(dotpos) != '.') {
                dotpos = caret;
                //dump("changed dot pos: " + dotpos + "\n");
            }

            // look backwards for a non-variable-name character
            spacepos = findbeginning(_in.value, dotpos - 1, false);
            //dump("space pos: " + spacepos + "\n");
            // get the object we're trying to complete on
            if (spacepos == dotpos || spacepos + 1 == dotpos || dotpos == caret) {
                // try completing function args
                if (_in.value.charAt(dotpos) == '(' ||
                    (_in.value.charAt(spacepos) == '(' && (spacepos + 1) == dotpos)) {
                    var fn, fname;
                    var from = (_in.value.charAt(dotpos) == '(') ? dotpos : spacepos;
                    spacepos = findbeginning(_in.value, from - 1, false);

                    fname = _in.value.substr(spacepos + 1, from - (spacepos + 1));
                    //dump("fname: " + fname + "\n");
                    try {
                        with(_win.Shell._scope)
                        with(_win)
                        with(shellCommands)
                        fn = eval(fname);
                    } catch (er) {
                        //dump('fn is not a valid object\n');
                        return;
                    }
                    if (fn == undefined) {
                        //dump('fn is undefined');
                        return;
                    }
                    if (fn instanceof Function) {
                        // Print function definition, including argument names, but not function body
                        if (!fn.toString().match(/function .+?\(\) +\{\n +\[native code\]\n\}/))
                            println(fn.toString().match(/function .+?\(.*?\)/), "tabcomplete");
                    }

                    return;
                } else
                    obj = _win;
            } else {
                var objname = _in.value.substr(spacepos + 1, dotpos - (spacepos + 1));
                //dump("objname: |" + objname + "|\n");
                try {
                    with(_win.Shell._scope)
                    with(_win)
                    obj = eval(objname);
                } catch (er) {
                    printError(er);
                    return;
                }
                if (obj == undefined) {
                    // sometimes this is tabcomplete's fault, so don't print it :(
                    // e.g. completing from "print(document.getElements"
                    // println("Can't complete from null or undefined expression " + objname, "error");
                    return;
                }
            }
            //dump("obj: " + obj + "\n");
            // get the thing we're trying to complete
            if (dotpos == caret) {
                if (spacepos + 1 == dotpos || spacepos == dotpos) {
                    // nothing to complete
                    //dump("nothing to complete\n");
                    return;
                }

                complete = _in.value.substr(spacepos + 1, dotpos - (spacepos + 1));
            } else {
                complete = _in.value.substr(dotpos + 1, caret - (dotpos + 1));
            }
            //dump("complete: " + complete + "\n");
            // ok, now look at all the props/methods of this obj
            // and find ones starting with 'complete'
            var matches = [];
            var bestmatch = null;
            for (var a in obj) {
                //a = a.toString();
                //XXX: making it lowercase could help some cases,
                // but screws up my general logic.
                if (a.substr(0, complete.length) == complete) {
                    matches.push(a);
                    ////dump("match: " + a + "\n");
                    // if no best match, this is the best match
                    if (bestmatch == null) {
                        bestmatch = a;
                    } else {
                        // the best match is the longest common string
                        function min(a, b) {
                            return ((a < b) ? a : b);
                        }
                        var i;
                        for (i = 0; i < min(bestmatch.length, a.length); i++) {
                            if (bestmatch.charAt(i) != a.charAt(i))
                                break;
                        }
                        bestmatch = bestmatch.substr(0, i);
                        ////dump("bestmatch len: " + i + "\n");
                    }
                    ////dump("bestmatch: " + bestmatch + "\n");
                }
            }
            bestmatch = (bestmatch || "");
            ////dump("matches: " + matches + "\n");
            var objAndComplete = (objname || obj) + "." + bestmatch;
            //dump("matches.length: " + matches.length + ", tooManyMatches: " + tooManyMatches + ", objAndComplete: " + objAndComplete + "\n");
            if (matches.length > 1 && (tooManyMatches == objAndComplete || matches.length <= 10)) {

                printWithRunin("Matches: ", matches.join(', '), "tabcomplete");
                tooManyMatches = null;
            } else if (matches.length > 10) {
                println(matches.length + " matches.  Press tab again to see them all", "tabcomplete");
                tooManyMatches = objAndComplete;
            } else {
                tooManyMatches = null;
            }
            if (bestmatch != "") {
                var sstart;
                if (dotpos == caret) {
                    sstart = spacepos + 1;
                } else {
                    sstart = dotpos + 1;
                }
                _in.value = _in.value.substr(0, sstart) +
                    bestmatch +
                    _in.value.substr(caret);
                setselectionto(_in, caret + (bestmatch.length - complete.length));
            }
        }
    }

    function printQuestion(q) {
        println(q, "input");
    }

    function printAnswer(a) {
        if (a !== undefined) {
            a.split('\n').forEach(function (line,index) { 
              if (opts.cmd && line[0]=='@') {
                opts.cmd(line);
              } else {
                println(line, "normalOutput", options.log.Time && index==0) 
              }
            });
            shellCommands.ans = a;
        }
    }

    function printError(er) {
        var lineNumberString;

        lastError = er; // for debugging the shell
        if (er.name) {
            // lineNumberString should not be "", to avoid a very wacky bug in IE 6.
            lineNumberString = (er.lineNumber != undefined) ? (" on line " + er.lineNumber + ": ") : ": ";
            println(er.name + lineNumberString + er.message, "error"); // Because IE doesn't have error.toString.
        } else
            println(er, "error"); // Because security errors in Moz /only/ have toString.
    }

    function go(s) {
        _in.value = question = s ? s : _in.value;

        if (question == "")
            return;

        histList[histList.length - 1] = question;
        histList[histList.length] = "";
        histPos = histList.length - 1;

        // Unfortunately, this has to happen *before* the JavaScript is run, so that
        // print() output will go in the right place.
        _in.value = '';
        recalculateInputHeight();
        printQuestion(question);

        if (_win.closed) {
            printError("Target window has been closed.");
            return;
        }

        try {
            ("Shell" in _win)
        } catch (er) {
            printError("The JavaScript Shell cannot access variables in the target window.  The most likely reason is that the target window now has a different page loaded and that page has a different hostname than the original page.");
            return;
        }

        if (!("Shell" in _win))
            initTarget(); // silent

        // Evaluate question using _win's eval (this is why eval isn't in the |with|, IIRC).
        // _win.location.href = "javascript:try{ Shell.printAnswer(eval('with(Shell._scope) with(Shell.shellCommands) {' + Shell.question + String.fromCharCode(10) + '}')); } catch(er) { Shell.printError(er); }; setTimeout(Shell.refocus, 0); void 0";
        options.cmd.exec(question)
    }

    function ask(cmd,data,callback) {
      options.cmd.exec(cmd)
    }  
    
    function cmd () {
      return options.cmd
    }
    
    // API
    return {
        ask:ask,
        cmd:cmd,
        commands: shellCommands,
        init: init,
        inputKeydown: inputKeydown,
        options:options,
        print:printAnswer,
        setup:setup
    }
};
