/* borax.js: Client side of the Borax web scripting framework.
Copyright (C) 2006  Ian Kjos

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the

Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor
Boston, MA  02110-1301  USA
*/
navigator.isWonky = navigator.appVersion.match(/Konqueror|Safari|KHTML/)
navigator.isIE = navigator.appVersion.match(/MSIE/)


if (navigator.isIE) window.onunload = function() {
	/* IE leaks memory when JS objects and DOM nodes contain
	reference cycles, because the two types of objects are
	kept in separate garbage-collection heaps.
	
	To prevent such leaks in IE, we need to disconnect the
	DOM and JS objects from one another.
	
	One method is as follows:
	*/
	
	var all = document.getElementsByTagName("*")
	for (var i in all) E.wipe(all[i])
	E.wipe(window)
	/* This has the advantage of simplicity, but it's
	not completely general. If you've used nonstandard
	event names or developed memory cycles in other ways,
	then this won't solve the problem.
	
	What might help is some cache of links between DOM nodes
	and JS elements, but this is probably an expensive proposition
	which would only help in unusual cases.
	
	The above should solve the vast majority of leaks, thus
	keeping IE usable for rather a while longer.
	*/
}
var things_to_do_first = [];
function first_do(task) {
	things_to_do_first.push(task)
}

window.onload = function () {
	var job = things_to_do_first;
	for (var i in job) job[i]();
}
function Q() {
	this.q = []
	this.lock = false
}
Q.prototype = {
	each: function(f) {
		for (k in this.q) f(k, this.q[k])
	},
	add: function(fn) {
		var has=false
		this.each(function(k,v){if(v==fn)has=true})
		if (!has) this.q.push(fn)
	},
	del: function(fn) {
		 // remove fn from this.q, if it's there.
		 this.each(function(k,v){if(v==fn)delete this.q[k]})
	},
	dispatch: function(evt) {
		if (this.lock) return; // FIXME: complain about an event broadcast cycle
		this.lock = true
		this.each(function(k,v){v(evt)})
		this.lock = false
	}
}
var MISSING = -1

var misc = {
	// We occasionally need probably unique strings.
	puid: function(suffix) {
		return 'puid_'+(misc.counter++)+suffix
		// A better model might use dates or somesuch.
	},
	counter: 1
}

function encode(param) {
	var a = []
	for (var i in param) a.push(encodeURIComponent(i) + '=' + encodeURIComponent(param[i]))
	return a.join('&');
}

function url(base, param) {
	if (!param) return base;
	var enc = encode(param);
	if (base.indexOf('?') == MISSING) return base + '?' + enc;
	else return base + '&' + enc;
}

var O = {
	merge : function() {
		var a = {}
		for (var i=0; i < arguments.length; i++) {
			// The arguments array doesn't provide "for (i in foo)..."
			var x = arguments[i]
			if (x) for (var k in x) a[k] = x[k]
		}
		return a
	},
	extend : function(dst, src) {
		for (var k in src) dst[k] = src[k]
	}
}
var raw = {
	// Send a form and get result behind the scenes
	submitForm: function (form, callback) {
		var name = misc.puid('iframe')
		// It's extremely unlikely that such a name already exists.
		var d = T('div')
		E.hide(d)
		d.innerHTML = '<iframe name="'+name+'" onload="raw.respondForm(this)" onabort="setTimeout(this.parentNode.die, 0)"></iframe>'
		document.body.appendChild(d)
		d.callback = callback
		d.die = function() {
			var i = d.firstChild
			i.onload = null;
			i.onabort = null;
			d.innerHTML = ''
			document.body.removeChild(d)
		}
		form.target = name
		form.submit()
	},
	respondForm: function(i) {
		var d = i.parentNode
		var callback = d.callback
		d.callback = null
		var rs = window.frames[i.name].document.body.innerHTML
		setTimeout(d.die, 0)
		callback(rs);
	},
	// Get a well-decorated HTTP request object, cross-browser
	httpRequest: function (callback) {
		var request;
		if (window.XMLHttpRequest) {
			// We're on a modern Gecko browser
			request = new XMLHttpRequest();
		} else {
			// We're probably looking at MSIE
			request =	new ActiveXObject("Msxml2.XMLHTTP") ||
				new ActiveXObject("Microsoft.XMLHTTP");
		}
		request.onreadystatechange = function () {
			if (request.readyState == 4) {
				if (request.status == 200) callback(request.responseText);
				else Borax.croak("HTTP error "+request.status+" ("+request.statusText+") happened.", request.responseText);
			}
		}
		// request.stop = request.abort;
		return request;
	},
	// Do a HTTP GET request, but no complex processing thereof
	httpGet: function (url, callback) {
		var request = raw.httpRequest(callback)
		request.open('GET', url, true);
		request.send('')
		return request
		/*	I have chosen to use the HttpRequest strategy,
			but if that were unavailable, I could theoretically
			fall back to using something like the iframe
			form posting technique above. YAGNI.
		*/
	}
	/* I don't have a POST method here, because:
	1. I already coded a perfectly suitable iframe method above.
	2. People might want too many subtle differences.
	
	Therefore, the raw post method is left as an exercise for
	the reader. In case you want to know, here are the steps:
	
	var request = raw.HttpRequest(callback)
	request.open('POST', url, true)
	request.setRequestHeader(
		"Content-Type",
		"application/x-www-form-urlencoded"
	)
	request.send('some urlencoded string of parameters')
	
	Now, there are in fact many ways to vary this general pattern.
	You might change the request method to HEAD, or even some webdav
	shenanigan.
	*/
}
var json = {
	/*	I've chosen JSON for its simplicity and speed as the
		preeminent encoding for getting data from the server to
		the client. See http://json.org/ for further reference.
	*/
	// The core of the response protocol:
	respond: function (url, callback, text) {
		var signal = text.substr(0, 1);
		var result = '(' + text.substr(1) + ')';
		if (signal == '+') callback(eval(result));
		else if(signal == '-') {
			var gripe = eval(result);
			Borax.croak(gripe['error'], gripe['detail']);
		}
		else Borax.croak('Server Barf: '+url, text);
	},
	// We need to wrap that up in a blanket now and again:
	bind: function (url, callback) {
		return function(text) { json.respond(url, callback, text) }
	},
	// Let's do a HTTP GET and treat the result with json:
	httpGet: function (url, callback) {
		return raw.httpGet(url, json.bind(url, callback))
	},
	// And the same for a form submission:
	submitForm: function (form, callback) {
		return raw.submitForm(form, json.bind(form.action, callback))
	}
}
function Toggle(init) {
	/* The simplest possible generic state machine object
	
	You can set the onset and onreset properties of the
	toggle object to determine what happens when the toggle
	changes state.
	
	The state change functions will not double-fire if you
	sent two successive "set" or "reset" messages.
	
	*/
	var state = init ? true : false;
	var o = {
		toggle: function() {
			state = !state
			if (state) o.onset && o.onset()
			else o.onreset && o.onreset()
		},
		set: function() { if (!state) o.toggle() },
		reset: function() { if (state) o.toggle() },
		state: function() {return state},
		go: function (s) { if (state != s) o.toggle() }
	}
	return o
}
function $() {
  var elements = [];

  for (var i=0; i < arguments.length; i++) {
    var element = arguments[i];
    if (typeof element == 'string')
      element = document.getElementById(element);

    if (arguments.length == 1)
      return element;

    elements.push(element);
  }

  return elements;
}

function T(name, attrs, inside) {
	var t = document.createElement(name);
	for (var k in attrs) {
		var v = attrs[k]
		if (typeof v == 'function') t[k]=v	// IE MEMLEAK
		else if (typeof v == 'object') O.extend(t[k], v)
		else t.setAttribute(k, v)
	}
	T.add(t, inside)
	return t;
}
O.extend(T, {
	add: function(node, inside) {
		switch (typeof inside) {
			case '':
			break
			
			case 'object':
			try {
				node.appendChild(inside)
			} catch (ex) {
				if (inside instanceof Array) {
					for (k in inside) T.add(node, inside[k])
				} else {
					node.appendChild(document.createTextNode(inside))
				}
			}
			break
			
			case 'string':
			case 'number':
			node.appendChild(document.createTextNode(inside))
			break
			
			case 'undefined':
			// Do nothing.
			break
			
			default:
			alert('Teach T.add() about inside type '+typeof inside);
		}
	},
	btn: function(caption, command) {
		return T('button', {type: 'button', onclick: command}, caption)
	},
	hid: function(n,v) {
		return T('input', {type: 'hidden', name: n, value: v})
	},
	chk: function(n, v, c, attrs) {
		var a = O.merge(attrs, {
			type: 'checkbox',
			name: n,
			value: v
		})
		if (c) a.checked = 1;
		return T('input', a);
	},
	debug: function(o, recurse) {
		var a = []
		for (var k in o) try {
			a.push(T.debugProperty(k, o[k], recurse))
		} catch (ex) {
			a.push(T.debugException(k, ex, recurse))
		}
		return T('ul', {}, a)
	},
	debugProperty: function(n, v, recurse) {
		var a = [n+': ']
		switch (typeof v) {
			case 'object':
			a.push(v instanceof Array ? '[array]' :'{object}')
			if (recurse > 0) a.push(T.debug(v, recurse - 1))
			else a.push(' (unexplored)')
			break
			
			case 'function':
			a.push('<<function>>')
			break
			
			default:
			a.push(v.toString())
		}
		return T('li', {}, a);
	},
	debugException: function(k) {
		return T('li', {}, ['could not read: ', k]);
	}
})
var E = {
	isHidden: function (domNode) {
		return domNode.style.display == 'none'
	},
	show: function(e) {
		var h = E.isHidden(e)
		e.style.display = 'block'
		if (h) e.onshow && e.onshow()
	},
	hide: function(e) {
		var h = E.isHidden(e)
		e.style.display = 'none'
		if (!h) e.onhide && e.onhide()
	},
	events: 'abort blur change click close dragdrop '+
			'error focus keydown keypress keyup load '+
			'mousedown mousemove mouseout mouseover mouseup '+
			'paint reset resize scroll select submit unload '+
			// Some custom events that I've added (as above)
			'show hide'.split(' '),
	wipe: function(e) {
		for (var evt in E.events) {
			var hook = 'on' + E.events[evt]
			delete e[hook]
		}
	}
}
var Borax = {
/* Let this part handle the DHTML stuff. In fact, I might just rename it.

*/
}
Borax.croak = function (error, detail) {
	var d=T('div', {style:{
		position: navigator.isIE ? 'absolute' : 'fixed',
		top: '25%', left: '20%', width: '60%',
		color: 'black', backgroundColor: '#ffdddd',
		padding: '5px', border: '1px solid black',
		opacity: '0.9', zIndex: '99999'
	}}, '');
	
	var m = T('span', {}, error || 'System Error');
	m.style.fontSize='14pt';
	d.appendChild(m);
	
	if (detail) {
		var h = navigator.isIE ? '200px' : window.innerHeight/3+'px'
		var p = T('pre', {style: {
			border: "1px solid green",
			overflow: 'scroll', height: h,
			padding: '3px', marginTop: '2px',
			position: 'relative'
		}}, detail);
		d.appendChild(p);
		d.appendChild(T.btn('Pop Out', function() {
			Borax.dialog(error, '<pre>'+p.innerHTML+'</pre>')
			d.parentNode.removeChild(d)
		}))
	}
	
	var b = T.btn('ARGH!', function(){d.parentNode.removeChild(d)})
	var c = T('center', {}, [ T('hr', {}), b ])
	d.appendChild(c);
	
	document.body.appendChild(d)
	b.focus();
}
Borax.dialog = function(title, content, h, w) {
	if (!h) h = 640
	if (!w) w = 840
	var dlg = window.open('', '', 'dialog,scrollbars=yes,height='+h+',width='+w)
	dlg.document.body.innerHTML = content
	// w.document.title = title
	return w
}
function Panel(front) {
	var self = document.createElement('div');
	if (front.parentNode) {
		front.parentNode.replaceChild(self, front)
	}
	self.appendChild(front);
	
	self.front = front;
	self.pane = {};
	self.current = self.front;
	self.show = function(el) {
		self.current.style.display = 'none';
		self.current = el;
		self.current.style.display = 'block';
	}
	self.showFront = function() {
		self.show(self.front);
	}
	self.showPane = function(name) {
		self.show(self.pane[name]);
	}
	self.makePane = function(name) {
		var p = document.createElement('div');
		p.style.display='none';
		self.pane[name] = p;
		self.appendChild(p);
		return p;
	}
	return self;
}

function ToggleButton(falseText, trueText, initState) {
	// A tool for a bistable UI command button
	var btn = T.btn('Colapse', function(){this.toggle.toggle()})
	btn.toggle = Toggle(initState)
	btn.toggle.onreset = function() {
		if (btn.onreset) btn.onreset()
		btn.innerHTML = ''
		T.add(btn, falseText)
	}
	btn.toggle.onset = function() {
		if (btn.onset) btn.onset()
		btn.innerHTML = ''
		T.add(btn, trueText)
	}
	return btn
}
function CollapseButton(DOMElement) {
	// A handy tool for hiding parts of a document
	
	var btn = ToggleButton('Expand', 'Colapse', true)
	btn.onreset = function() { E.hide(DOMElement) }
	btn.onset = function() { E.show(DOMElement) }
	return btn
}
var style = {
	// Get around CSS class attribute limitation in IE,
	// and eventually use this same framework to write CSS classes
	// in a standard way.
	classes: {},
	learn: function(cls, cssHash) {
		if (('cssFloat' in cssHash) && navigator.isIE)
			cssHash.styleFloat = cssHash.cssFloat
		style.classes[cls] = cssHash
	},
	tag: function(name, cls) {
		var tag = document.createElement(name)
		tag.className = cls
		return tag
	},
	div: function(cls) {
		return style.tag('div', cls)
	}
}
