// ==UserScript==
// @name           Minibuffer
// @namespace      http://white.s151.xrea.com/
// @description    Minibuffer with some commands
// @include        *
// ==/UserScript==


function trace() {
	var fn;
	if ( typeof Firebug == 'undefined' ) {
		fn = console;
	} else if ( Firebug && Firebug.Console && Firebug.Console.log  ) {
		fn = Firebug.Console;
	}

	if ( fn ) {
		if ( arguments.length == 1 ) {
			fn.log(arguments[0]);
		} else {
			var args = [];
			for (var i = 0; i < arguments.length; i++) {
				args.push(arguments[i]);
			}
			fn.log(args);
		}
	}
}
var Minibuffer = {
  KEYBIND : {
	  'C-m'  : 'bindDoAndExit',
	  'RET'  : 'bindDoAndExit',
	  'ESC'  : 'bindExit',
	  'C-g'  : 'bindExit',
	  'C-['  : 'bindExit',
	  'C-n'  : 'bindSelectNext',
	  'Down' : 'bindSelectNext',
	  'C-p'  : 'bindSelectPrevious',
	  'Up'   : 'bindSelectPrevious',
	  'TAB'  : 'bindComplete',
	  'C-i'  : 'bindComplete',
	  'C-h'  : 'bindDeleteBackwardChar',
	  'BAC'  : 'bindDeleteBackwardChar',
	  'C-u'  : 'bindDeleteAllStrings',
	  'C-w'  : 'bindDeleteBackwardWord',
	  'A-h'  : 'bindDeleteBackwardWord',
  },
  prompt: 'M-x:',
  current: -1,
  version: '2007.08.28',

  htmlprompt: null,
  container : null,
  completion : null,
	
  enable: false,
  init : function() {
	  if(Minibuffer.enable) return;
	  Minibuffer.enable = true;
	  
	  var prompt_s = getspan([Minibuffer.prompt]);
	  Minibuffer.htmlprompt = prompt_s;
	  
	  var input = getspan([]);
	  input.id  = 'gm_minibuffer_input_area';
	  Minibuffer.input = input;

	  var completion = document.createElement('ul');
	  completion.id  = 'gm_minibuffer_completion';
	  Minibuffer.completion = completion;

	  var container = getdiv([completion, prompt_s, input]);
	  container.id  = 'gm_minibuffer_container';
	  Minibuffer.container = container;

	  var id  = '#' + container.id;
	  var cid = '#' + completion.id;
	  var iid = '#' + input.id;
	  var inherit = 'background:inherit; background-image:inherit; background-color:inherit; color:inherit; text-align:inherit; font-size:inherit; font-style;inherit; font-weight:inherit; magrin:inherit; opacity:inherit; text-decoration:inherit; border:0px; height:100%; padding:0; margin:inherit; font-family:inherit; vertical-align:inherit; line-height:inherit; font-stretch:inherit; font-variant:inherit; font-size-adjust:inherit; letter-spacing:inherit;';
	  GM_addStyle([id,'{', 'right: 0px;', 'left: 0px;', 'bottom: 0px;', 'line-height: 100%;', 'vertical-align: baseline;', 'border: 1px dotted #444;', 'font-family: sans-serif;', 'text-decoration: none;', 'font-weight: normal;', 'font-style: normal;', 'font-size: medium;', 'font-stretch: normal;', 'font-variant: normal;', 'font-size-adjust: none;', 'letter-spacing: normal;', 'background: none;', 'text-align: left;', 'position: fixed;', 'margin: 0;', 'padding: 20px;', 'background-color: #000;', 'background-image: none;', 'color: #aaa;', '-moz-border-radius: 10px 10px 0px 0px;', 'opacity:0.8;', 'z-index:999;', '}\n',
				   id, ' > span',iid, '{', 'color: #CB6161;','display:inline;','}',
				   id, ' > span {', inherit, 'color: #ccc;', 'display:inline;','margin-right: 5px;','}\n',
				   cid, '{', inherit, 'margin-bottom: 20px;','border-bottom: 1px dotted #444;' ,'}\n',
				   cid, ' > span {', inherit, 'color: #ccc;', 'margin: 10px;','display:block;','}\n',
				   cid, ' > li {', inherit, 'color: #ccc;', 'padding: 2px;','margin-bottom:10px;','margin-left: 10px;','}\n',
				   cid, ' > li.gm_minibuffer_selected{', inherit, 'color: #CB6161;', 'padding: 2px;','margin-bottom:10px;','margin-left: 10px;','}\n',
				   ].join(''));
  },

  last_completed_string: null,
  getCompletedString : function(list){
	  if(!Minibuffer.last_completed_string || Minibuffer.last_completed_string == '') return false;
	  var lst = Minibuffer.last_completed_string.split('\n');
	  if(lst.length == 1) return lst[0];
	  var str_c = '';
	  var str_a = lst[0];
	  for(var i=1; i<lst.length; i++){
		  var str_b = lst[i];
		  for(var j=1; j<=str_a.length; j++){
			  if(str_a.slice(0,j) == str_b.slice(0,j)) str_c = str_a.slice(0,j);
			  else break;
		  }
		  str_a = str_c
		}
	  return str_a;
  },

  updateComplationList : function(){
	  var input_str = Minibuffer.input.innerHTML;
	  var comp = Minibuffer.completion;
	  var old_candidate_lst = Minibuffer.candidates;
	  var candidate_lst = [];
	  var test = function(str, l){
		  var regexp = new RegExp(str, "i");
		  if(typeof(l) == 'undefined') l=Minibuffer.candidates;
		  for(var i=0; i<l.length; i++) if(l[i].match(regexp)) candidate_lst.push(l[i]);
	  }
	  if(input_str == ''){
		  candidate_lst = old_candidate_lst;
	  }else if(input_str.match(' ')){
		  var strings = input_str.split(' ');
		  var tmplst = Minibuffer.candidates;
		  for(var j=0; j<strings.length; j++){
			  if(!tmplst.length) break;
			  test(strings[j] ,tmplst);
			  tmplst = candidate_lst;
			  candidate_lst = [];
		  }
		  candidate_lst = tmplst;
	  }else{
		  test('^' + input_str.replace(/^\^/, '').escapeRegexp());
		  if(!candidate_lst.length) test(input_str.escapeRegexp());
	  }
	  var completed_str = candidate_lst.join('\n');
	  if(candidate_lst.length && Minibuffer.last_completed_string != completed_str) {
		  comp.innerHTML = '';
		  for(var i=0; i<candidate_lst.length; i++) comp.appendChild(getli([candidate_lst[i]]));
	  }else if(!candidate_lst.length){
		  comp.innerHTML = '';
	  }
	  Minibuffer.last_completed_string = completed_str;
	  Minibuffer.current = -1;
  },

  candidates : [],
  callbackExit : null,
  candidate_command_hash : {},
  getCandidateCommandHash : function(){
	  return  Minibuffer.candidate_command_hash;
  },
  complete: function(candidates, callbackExit, prompt) {
	  Minibuffer.init();
	  if(prompt) Minibuffer.htmlprompt.innerHTML = prompt;
	  Minibuffer.callbackExit = callbackExit;
	  Minibuffer.candidate_command_hash = candidates;
	  for(x in candidates) Minibuffer.candidates.push(x);
	  document.body.appendChild(Minibuffer.container);
	  document.addEventListener('keypress', Minibuffer.handleKey, true);
  },

  deleteAllStrings: function(){
	  Minibuffer.last_completed_string = '';
	  Minibuffer.input.innerHTML = '';
	  Minibuffer.completion.innerHTML = '';
  },
  exit : function(){
	  Minibuffer.current = -1;
	  Minibuffer.deleteAllStrings();
	  Minibuffer.candidates = [];
	  Minibuffer.htmlprompt.innerHTML = Minibuffer.prompt;
	  document.body.removeChild(Minibuffer.container);
	  document.removeEventListener('keypress', Minibuffer.handleKey, true);
	  Minibuffer.callbackExit();
  },
  selectCandidate : function(oldNode, newNode){
	  Minibuffer.input.innerHTML = newNode.innerHTML;
	  newNode.setAttribute('class','gm_minibuffer_selected');
	  if(oldNode) oldNode.removeAttribute('class',0);
  },

  handleKey : function(aEvent) {
	  var key  = Keyboard.getKey(aEvent);
	  var func = Minibuffer.KEYBIND[key];
	  var preventDefault = true;
	  if(func) Minibuffer[func]();
	  else if(key.length == 1) Minibuffer.bindInputChar(key);
	  else preventDefault = false;
	  if(preventDefault) {
		  aEvent.preventDefault();
		  aEvent.stopPropagation();
	  }
  },
  bindInputChar : function(key){
	  Minibuffer.input.innerHTML = Minibuffer.input.innerHTML + key;
	  Minibuffer.updateComplationList();
  },
  bindDoAndExit : function(){
	  var commands = this.__Shell.parse(Minibuffer.input.innerHTML);
	  //FIXME: check command existence first, and  exit and execute them all.
	  Minibuffer.exit();
	  var stdin = null;
	  var ret = commands.forEach( function ( command ) {
	  	var cmd = Minibuffer.getCandidateCommandHash()[ command.name ];
	  	if(!cmd) {
	  	  	return null;
	  	} else {
	  		stdin = cmd(command.name, command.args, stdin);
	  	}
	  } );
  },
  bindExit : function(){
	  Minibuffer.exit();
  },
  bindComplete : function(){
	  var func = function(){
		  var str = Minibuffer.getCompletedString();
		  if(str) {
			  Minibuffer.input.innerHTML = str;
		  }
		  return str;
	  }
	  if(!func()) {
		  Minibuffer.updateComplationList();
		  var lst = Minibuffer.last_completed_string.split('\n');
		  if(lst.length == 1) Minibuffer.bindComplete();
	  }
  },
  bindDeleteBackwardChar : function(){
	  Minibuffer.input.innerHTML = Minibuffer.input.innerHTML.slice(0, -1);
	  Minibuffer.updateComplationList();
  },
  bindDeleteBackwardWord : function(){
	  Minibuffer.input.innerHTML = Minibuffer.input.innerHTML.replace(/[^a-zA-Z0-9]*$/,'').replace(/[a-zA-Z0-9]*$/,'');
	  Minibuffer.updateComplationList();
  },
  bindDeleteAllStrings : function(){
	  Minibuffer.deleteAllStrings();
  },

  bindSelectNext : function(){
	  if(!Minibuffer.last_completed_string) Minibuffer.updateComplationList();
	  var last = Minibuffer.current != -1 && Minibuffer.completion.childNodes[Minibuffer.current];
	  Minibuffer.current++;
	  if(Minibuffer.current >= Minibuffer.completion.childNodes.length) Minibuffer.current = 0;
	  Minibuffer.selectCandidate(last, Minibuffer.completion.childNodes[Minibuffer.current]);
  },
  bindSelectPrevious : function(){
	  if(!Minibuffer.last_completed_string) Minibuffer.updateComplationList();
	  var last = Minibuffer.current != -1 && Minibuffer.completion.childNodes[Minibuffer.current];
	  Minibuffer.current--;
	  if(Minibuffer.current < 0) Minibuffer.current = Minibuffer.completion.childNodes.length-1;
	  Minibuffer.selectCandidate(last, Minibuffer.completion.childNodes[Minibuffer.current]);
  },
}

var Keyboard = {
  dict : {
	8:  'BAC',
	9:  'TAB',
	13: 'RET',
	27: 'ESC',
	38: 'Up',
	40: 'Down',
  },
  getKey : function(aEvent){
	  var keycode = aEvent.which;
	  var key = String.fromCharCode(keycode).toLowerCase();
	  key = (aEvent.ctrlKey) ? "C-"+key : key;
	  key = (aEvent.altKey || aEvent.metaKey) ? "M-"+key : key;
	  key = Keyboard.dict[aEvent.keyCode] || key;
	  return key;
  },
}

var getdiv = function(children){
	var div = document.createElement('div');
	children.forEach(function(child){if(child) div.appendChild(typeof(child) == 'string' ? document.createTextNode(child): child)});
	return div;
}
var getspan = function(children){
	var span = document.createElement('span');
	children.forEach(function(child){if(child) span.appendChild(typeof(child) == 'string' ? document.createTextNode(child): child)});
	return span;
}
var getli = function(children){
	var span = document.createElement('li');
	children.forEach(function(child){if(child) span.appendChild(typeof(child) == 'string' ? document.createTextNode(child): child)});
	return span;
}
String.prototype.escapeRegexp = function(){
	return this.replace(/^\?/, '.?').replace(/^\*/, '.*').replace(/\([^)]*$/, '').replace(/\[[^\]]*$/, '');
}
Array.prototype.find = function(obj){
	var test;
	if(typeof(obj) == 'function') test = obj
	else test = function(a){return a == obj}
	
	for(var i=0;i<this.length; i++) if(test(this[i])) return this[i];
	return false;
}

function log(message) {
	if(unsafeWindow && unsafeWindow.console) {
		unsafeWindow.console.log(message);
	}
}

var Command = {
  name_command_hash : {},
  name_list : [],
  add: function(hash){ // hash = { name: function, ,,, }
	  for(name in hash){
		  Command.name_command_hash[name] = hash[name];
		  Command.name_list.push(name);
	  }
  },
  complete : function(){
	  Command.removeEvent();
	  Minibuffer.complete(Command.name_command_hash ,Command.attachEvent);
  },
  init : function(){
	  Command.add({
		  'minibuffer-exit': Command.removeEvent,
	  });
	  Command.attachEvent();
  },
  handleKey : function (aEvent){
	  if(Keyboard.getKey(aEvent) == 'M-x')
		Command.complete();
  },
  attachEvent : function (arg){document.addEventListener('keypress', Command.handleKey, true);},
  removeEvent : function (arg){document.removeEventListener('keypress', Command.handleKey, true);},
}


Minibuffer.__Shell = {
	TT: {
		arg:  'arg',
		control: 'control'
	},
	Parser: {
		buffer: null,
		init: function (buffer) {
			this.buffer = buffer;
		},
		get_token: function (  ) {
			var surround =  function (s, c) {
				var d = (c.length > 1 ? c[1] : c) ;
				return c[0] + s + d;
			};

			var quote_chars = '"' + "'";
			var meta_chars = "|<>;";
			var quoted_arg = '(([' + quote_chars + '])(.+?)(\\3))';
			var bare_arg = surround( '^' + meta_chars + quote_chars + '\\s', '[]') + '+';
			var job_controlers = meta_chars;

			var exp = '^\\s*';
			exp += surround( [
								quoted_arg, surround(bare_arg, "()"),
											surround( surround(meta_chars, "[]"), "()" )
							].join("|"), "()" );
			var re = new RegExp(exp);
			if ( this.buffer.match(re) ) {
				// huuum, we need to count parenthesis index from constructed expression....
				// ^\s*(((["'])(.+?)(\3))|([^|<>;"']+)|([|<>;]))
				//4 or 6 or 7
				var token = RegExp.$4 ? {type: Minibuffer.__Shell.TT.arg, literal: RegExp.$4} : 
							RegExp.$6 ? {type: Minibuffer.__Shell.TT.arg, literal: RegExp.$6} : 
								{type: Minibuffer.__Shell.TT.control, literal: RegExp.$7 };
				this.buffer = RegExp.rightContext;
				return token;
			} else {
				return null;
			}
		},
	},
	Command: {
		commands: [],
		state: null,
		current_command: null,
		init: function () {
			this.state = this.need_command;
		},
		add_token: function (token) {
			this.state.apply(this, [token]);
		},
		need_command: function (token) {
			if ( token.type != Minibuffer.__Shell.TT.arg ) {
				trace("syntax error", token);
			} else {
				this.current_command = { name: token.literal, args: [] };
				this.state = this.search_for_end;
			}
		},
		search_for_end: function (token) {
			if ( token.type == Minibuffer.__Shell.TT.control ) {
				this.end();
			} else {
				this.current_command.args.push( token.literal );
			}
		},
		end: function () {
			this.commands.push(this.current_command);
			this.current_command = null;
			this.state = this.need_command;
		}
	},

	buffer: null,
	parse: function (buffer) {
		this.Parser.init(buffer);
		this.Command.init();

		var token;
		while ( token = this.Parser.get_token() ) {
			this.Command.add_token ( token );
		}
		this.Command.end();

		return this.Command.commands;
		//return this.execute( this.Command.commands);
	},
	execute: function (commands) {
		var obj = null;
		commands.forEach( function ( command ) {
			var proc = Bin[command.name];
			if ( proc ) {
				obj = proc.apply( this, [command.args, obj] );
			} else {
				trace("command not found.", command.name);
			}
		} );
		return stdin;
	}
}

if(document.body && top == self) {
	Command.init();
	window.Minibuffer = {
	  complete   : Minibuffer.complete,
	  addCommand : Command.add,
	};
}


// 
// add some sample commands.
// 
[
	{
		name:	"selected",
		command: function ( obj ) {
			var s = window.getSelection();
			
			var fragments = [];

			var n = s.rangeCount;
			for ( var i = 0; i < n ; i ++ ) {
				var range = s.getRangeAt(i);
				var documentFragment = range.cloneContents();

				var ret = document.createElement('root');
				ret.appendChild(documentFragment);

				fragments.push(ret);
			}

			obj.paragraphes = fragments;
			obj.links = [];
			return obj;
		},
		condition: function () { return true }
	},
	{
		name: 'images',
		command: function ( obj ) {
			var self = this;

			var anchor_nodes = [];

			obj.paragraphes.map( function (dom) {
				var pseudo_array = dom.getElementsByTagName('img');
				for ( var i = 0; i < pseudo_array.length ; i ++ ) {
						anchor_nodes.push( pseudo_array[i] );
				}
			} );
			obj.paragraphes =anchor_nodes;
			obj.links = anchor_nodes.map( function (a) { return a.src} );
			return obj;
		},
		condition: function () { return true }
	},
	{
		name: 'links',
		command: function ( obj ) {
			var self = this;

			var anchor_nodes = [];

			obj.paragraphes.map( function (dom) {
				var pseudo_array = dom.getElementsByTagName('a');
				for ( var i = 0; i < pseudo_array.length ; i ++ ) {
						anchor_nodes.push( pseudo_array[i] );
				}
			} );
			obj.paragraphes = anchor_nodes;
			obj.links = anchor_nodes.map( function (a) { return a.href} );
			return obj;
		},
		condition: function () { return true }
	},
	{
		name: 'echo',
		command: function (obj) {
			trace(obj);
			return obj;
		},
		condition: function () { return true }
	},
	{
		name: 'xpath',
		command: function (obj) {
			var exp = obj.args.shift();

			var nodes = obj.paragraphes.map( function ( node ) {
				return window.LDRize.$X(exp, node);
			} );
			obj.paragraphes = nodes;
			obj.links = new Array(nodes.length);
			return obj;
		},
		condition: function () { return true }
	},
	{
		name: 'grep',
		command: function (obj) {
			var regex = obj.args.shift();
			var modifier = obj.args.shift();

			var re = new RegExp(regex, modifier);

			var paragraphes = [];
			var links = [];

			for ( var i = 0; i < obj.links.length; i++ ) {
				var u = obj.links[i];
				if ( u.match( re ) ) {
					paragraphes.push( obj.paragraphes[i]);
					links.push(u);
				}
			}
			obj.paragraphes = paragraphes;
			obj.links = links;
			return obj;
		},
		condition: function () { return true }
	},
	{
		name: 'openintab',
		command: function (obj) {
			var target = obj.args.shift();

			var open_with = target ? GM_openInTab : window.open;

			obj.links.map( function ( url ) {
				trace( url );
				return ( target ) ?
							window.open( url, target ) : 
							GM_openInTab(url) ;
			} );
			return false;
		},
		condition: function () { return true }
	}

].forEach( function (cmd) { window.LDRize.addCommand( cmd )  } );



