var 
histList = [""], 
  histPos = 0, 
  _scope = {}, 
  _win, // a top-level context
  question,
  tooManyMatches = null;

var highlighter = null;

function refocus() {
  _in.blur(); // Needed for Mozilla to scroll correctly.
  _in.value = trim(_in.value);
  _in.focus();
  window.scrollTo(window.scrollX, window.scrollMaxY);
}

function init() {
  _in = document.getElementById("input");
  _out = document.getElementById("output");

  _win = window;

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

  _win.Shell = window;

  refocus();

  
  if (window.getSelection && document.addEventListener) {
    // Unless the user is selected something, refocus the textbox.
    // (requested by caillon, brendan, asa)
    function keepFocusInTextbox(e)  {
      g = e.target; 
      while (!g.tagName) 
	g = g.parentNode; 
      t = g.tagName.toUpperCase(); 
      if (t!="A" && t!="INPUT" && !String(window.getSelection())) 
	refocus(); 
    }
    document.addEventListener("click", keepFocusInTextbox, false);
  }

  highlighter = initHighlighter(); 
}

function initHighlighter() {
  var highlighter = new dp.sh.Brushes.JScript();
  highlighter.addGutter = true;
  highlighter.addControls = false;
  return highlighter;
}

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");
  },

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

  // 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) {
    var ns = ["Methods", "Fields", "Unreachables"];
    var as = [[], [], []]; // array of (empty) arrays of arrays!

    var protoLevels = 0;

    for (var p = e; p; p = p.__proto__) {
      for (var 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 (var 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 (var j=0; j<protoLevels; ++j)
      for (var i=0;i<ns.length;++i)
	if (as[i][j].length) 
	  printWithRunin(ns[i] + times(" of prototype", j), as[i][j].sort().join(", "), "propList");
  },

  // Blink is very Mozilla-only...
  blink : function blink(e)  {
    if (!e)			    throw("blink: e is null or undefined.");
    if (!e.TEXT_NODE)		    throw("blink: e must be a node.");
    if (e.nodeType == e.TEXT_NODE)  throw("blink: e must not be a text node");
    if (e.documentElement)	    throw("blink: e must not be the document object");

    function setOutline(e,o) { e.style.MozOutline = o; }
    function focusIt(a) { a.focus(); }

    if (e.ownerDocument)
      setTimeout(focusIt, 0, e.ownerDocument.defaultView.top);

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

    setTimeout(focusIt, 800, window);
  },

  scope : function scope(sc)  {
    if (!sc) sc = {};
    _scope = sc;
    println("Scope is now " + sc + ".  If a variable is not found in this scope, window will also be searched.	New variables will still go on window.", "message");
  },

  mathHelp : function mathHelp()  {
    printWithRunin("Math constants", "E, LN2, LN10, LOG2E, LOG10E, PI, SQRT1_2, SQRT2", "propList");
    printWithRunin("Math methods", "abs, acos, asin, atan, atan2, ceil, cos, exp, floor, log, max, min, pow, random, round, sin, sqrt, tan", "propList");
  },

  ans : undefined
};


function hist(direction) {
  // 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 (direction=="previous") {
    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("_in.value = ''; _in.value = histList[histPos];", 0);
    }
  } 
  if (direction == "next") {
    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;
  }

  function getcaretpos(inp)  {
    if(inp.selectionEnd)
      return inp.selectionEnd;

    if(inp.createTextRange) {
      //dump('using createTextRange\n');
      var docrange = _win.Shell.document.selection.createRange();
      var inprange = inp.createTextRange();
      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, theObjectThatwillNeverGiveRiseToNamingConfilctAgain;
    //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(Shell.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
	theObjectThatwillNeverGiveRiseToNamingConfilctAgain = _win;
    }
    else  {
      var objname = _in.value.substr(spacepos+1,dotpos-(spacepos+1));
      //trace("objname:"+objname);
      //dump("objname: |" + objname + "|\n");
      try {
	with(_win.Shell._scope)
	  with(_win)
	  theObjectThatwillNeverGiveRiseToNamingConfilctAgain = eval(objname);
      }
      catch(er) {
	printError(er); 
	return;
      }
      //trace("obj:"+obj);
      if(theObjectThatwillNeverGiveRiseToNamingConfilctAgain == undefined) {
	// sometimes this is tabcomplete's fault, so don't print it :(
	// e.g. completing from "print(document.getElements"
	println("Tab Complete Error: 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 theObjectThatwillNeverGiveRiseToNamingConfilctAgain) {
      //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 || theObjectThatwillNeverGiveRiseToNamingConfilctAgain) + "." + bestmatch;
    //dump("matches.length: " + matches.length + ", tooManyMatches: " + tooManyMatches + ", objAndComplete: " + objAndComplete + "\n");
    if(matches.length > 1 && (tooManyMatches == objAndComplete || matches.length <= 10)) {

      printWithRunin("Matches: ", matches.sort().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));
    }
  }
}

var lastInputKeyCode = 0;
function inputkey(event) {
  // Use onkeydown because IE doesn't support onkeypress for arrow keys
  // But because it's onkeydown and not onkeypress, we can't return false to prevent the
  // cursor from moving :(  That's what the setTimeout hack is for.
  var keyCode = event.keyCode;
  
  if(keyCode == 13 && (lastInputKeyCode == 13 || event.ctrlKey)) {
    go();
    return true;
  }
  else 
    lastInputKeyCode = keyCode;
  
  if(keyCode == 9) {// tab key
    tabcomplete();
    // refocus because tab was hit
    setTimeout("refocus()", 0);

    return false;
  }
  else if(keyCode == 38 && event.ctrlKey)
    hist("previous");
  else if(keyCode == 40 && event.ctrlKey)
    hist("next");
  
  
  return true;
}
  

function highlight(input) { 
  if(highlighter) {
    highlighter.Highlight(input);
    
    // place the result table inside a div
    var div = document.createElement('DIV');
    
    div.className = 'dp-highlighter';
    div.appendChild(highlighter.table);
    
    return div;
  }
  else 
    return null;
}

function println(s, type) {
  if(s=String(s)) {
    var newdiv = document.createElement("div");
    newdiv.appendChild(document.createTextNode(s));
    newdiv.className = type;
    _out.appendChild(newdiv);
    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 trim(s) {

  //this won't work on multiline input if RegExp.multiline is true
  var b = RegExp.multiline;
  RegExp.multiline = false;
  var ret =  s.replace(/^\s+/, '').replace(/\s+$/,'');
  RegExp.multiline = b;

  return ret;
}

function trace(s) {
  document.body.appendChild(document.createElement("div")).innerHTML = s;
}


function printQuestion(q) {
  var highlighted = highlight(q);
  if(highlighted){
    var histLen = histList.length - 2;
    highlighted.onclick = function(event){
      _in.value = histList[histLen];
    };
    _out.appendChild(highlighted);
  }
  else{
    println(q, "input")};
  }

function printAnswer(a) { if (a!==undefined) { println(a, (highlighter && "highlightOutput" || "normalOutput")); shellCommands.ans = a; } }
function printError(er) { 
  if (er.name)
    println(er.name + ": " + 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) {
  question = s ? s : _in.value;
  question = trim(question);
  _in.value = question;
  
  if (question == "")
    return;

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

  // Because of differences in timing among browsers, 
  // this should be the last statement in go().
  _win.location.href = "javascript:Shell._in.value=''; Shell.printQuestion(Shell.question); try{ with(Shell._scope) with(Shell.shellCommands) Shell.printAnswer(eval(Shell.question)); } catch(er) { Shell.printError(er); }; setTimeout(Shell.refocus, 0); void 0";
}
