// Emacs settings: -*- mode: Fundamental; tab-width: 4; -*-

////////////////////////////////////////////////////////////////////////////
//                                                                        //
// General-purpose Javascript functions and classes                       //
//                                                                        //
// Copyright (c) 2004-2009, Andrew Birrell                                //
//                                                                        //
////////////////////////////////////////////////////////////////////////////


//
// String manipulation
//

function utf8(str) {
	// Return a string whose charCodes are the UTF-8 bytes for the given
	// Unicode string "str".
	//
	return unescape(encodeURIComponent(str));
}

function caseSort(a, b) {
	// case-insensitive sort-order function
	//
	var la = a.toLowerCase();
	var lb = b.toLowerCase();
	return (la < lb ? -1 : (la > lb ? 1 : 0));
}


//
// Key-value cache with LRU replacement
//

function Cache(lruSize) {
	// Object constructor for a cache keeping at least
	// lruSize (and at most lruSize*2) most-recently-used entries.
	// Instantiate as an object, e.g. var myCache = new Cache(50);
	// then call methods, e.g. myCache.write("key", "value");
	//
	this.lruSize = lruSize;
	this.recent = new Object();
	this.old = new Object();
	this.count = 0;
}

Cache.prototype.write = function(key, value) {
	// Include given (key,value) in the cache
	//
	if (!this.recent[key]) this.count++;
	this.recent[key] = value;
	if (this.count >= this.lruSize) {
		this.old = this.recent;
		this.recent = new Object();
		this.count = 0;
	}
}

Cache.prototype.read = function(key) {
	// If key is in the cache, return value, else null
	//
	var value;
	if (value = this.recent[key]) return value;
	if (value = this.old[key]) {
		this.write(key, value);
		return value;
	}
	return null;
}

Cache.prototype.flush = function(key) {
	// ensure key is no longer in the cache
	//
	if (this.recent[key]) {
		this.recent[key] = null;
		this.count--;
	}
	if (this.old[key]) this.old[key] = null;
}


//
// HTTP support: encoding, cookies, query string
//

function htmlspecials(str) {
	// Return a string with critical HTML characters escaped
	//
	var res = str.replace(/&/g, '&amp;');
	res = res.replace(/[<]/g, '&lt;');
	res = res.replace(/>/g, '&gt;');
	res = res.replace(/"/g, '&quot;');
	return res;
}

function getCookie(key) {
	// If there's a cookie named "key" return its value, else false
	//
	var regExp = new RegExp("(^|.*; )" + key + "=");
	var value = document.cookie.replace(regExp, "");
	if (value == document.cookie) return null;
	return decodeURIComponent(value.replace(/;.*$/, ""));
}

function setCookie(key, value, expires, path, domain, secure) {
	// Set the cookie named "key" to "value"
	//
	document.cookie = key + "=" + encodeURIComponent(value) +
		(expires ? "; expires=" + expires.toGMTString() : "") +
		(path ? "; path=" + path : "") +
		(domain ? "; domain=" + domain : "") +
		(secure ? "; secure" : "");
}

function deleteCookie(key, path, domain, secure) {
	// Delete the cookie, by setting an obsolete expiry date
	//
	document.cookie = key + "=xxx" +
		"; expires=Fri, 31 Dec 1999 23:59:59 GMT" +
		(path ? "; path=" + path : "") +
		(domain ? "; domain=" + domain : "") +
		(secure ? "; secure" : "");
}

function getQueryArg(key) {
	// If our URL had a search string, and there's a key=value for given
	// key, return unescaped value; else return null
	//
	if (!location.search) return null;
	var regExp = new RegExp("^(\\?|.*&)" + key + "=");
	var value = location.search.replace(regExp, "");
	if (value == location.search) return null;
	return decodeURIComponent(value.replace(/&.*$/, ""));
}

function isIphone() {
	// Return true iff userAgent matches iPhone or iPod
	//
	var agent = navigator.userAgent;
	return agent && agent.match(/iPod;|iPhone;/i);
}


//
// Manipulating DOM elements
//

function showHide(id, show) {
	// Show or hide the DOM element with given ID (assumed to be a block)
	//
	var elt = document.getElementById(id);
	if (elt) elt.style.display = (show ? "block" : "none");
}

function showHideInline(id, show) {
	// Show or hide the DOM element with given ID (assumed to be inline)
	//
	var elt = document.getElementById(id);
	if (elt) elt.style.display = (show ? "inline" : "none");
}

function xableControl(id, yes) {
	var elt = document.getElementById(id);
	elt.disabled = !yes;
}

function getOptSelection(id) {
	// Return the current selection in a SELECT, or < 0 if none
	//
	var selector = document.getElementById(id);
	return selector.selectedIndex;
}

function setOptSelection(id, n) {
	// Set the current selection of a SELECT, or deselect if < 0
	//
	var selector = document.getElementById(id);
	selector.selectedIndex = n;
}

function getOptionPrompt(id) {
	// Return the text of the selected option in given selector
	//
	var selector = document.getElementById(id);
	var selected = selector.selectedIndex;
	if (selected < 0) return null;
	return selector.options[selected].text;
}

function getOption(id, andDelete) {
	// Return the value of the selected option in given selector, or null
	// if there's no selection.  Optionally, also delete the selected item.
	//
	var selector = document.getElementById(id);
	var selected = selector.selectedIndex;
	if (selected < 0) return null;
	var value = selector.options[selected].value;
	if (andDelete) selector.options[selected] = null;
	return value;
}

function setOption(id, value) {
	// Find the element of given SELECT object with given value,
	// make it the selected index, and return true.  If no such value
	// (including all cases where the argument is null), return false.
	//
	if (value === null) return false;
	var selector = document.getElementById(id);
	var options = selector.options;
	for (var i = 0; i < options.length; i++) {
		if (options[i].value == value) {
			selector.selectedIndex = i;
			return true;
		}
	}
	return false;
}

function appendOption(selector, prompt, value) {
	selector.options[selector.options.length] =
		new Option(prompt, value);
}

function insertOption(id, fixed, prompt, value, select) {
	// Insert option in sorted position, ignoring initial fixed area
	//
	var selector = document.getElementById(id);
	var options = selector.options;
	var pos = fixed;
	while (pos < options.length) {
		var old = options[pos].text;
		if (caseSort(old, prompt) > 0) break;
		pos++;
	}
	for (var i = options.length; i > pos; i--) {
		var old = options[i-1];
		options[i] = new Option(old.text, old.value);
	}
	options[pos] = new Option(prompt, value);
	if (select) selector.selectedIndex = pos;
}

function deleteOption(id, value) {
	// Delete option with given value from given selector
	//
	var selector = document.getElementById(id);
	var options = selector.options;
	for (var i = 0; i < options.length; i++) {
		if (options[i].value == value) {
			options[i] = null;
			break;
		}
	}
}

function truncateOptions(id, count) {
	// Truncate a selector to have this many options
	
	var selector = document.getElementById(id);
	var options = selector.options;
	while (options.length > count) options[options.length-1] = null;
}

function getCheckbox(id) {
	// Return whether checkbox "id" is currently checked
	//
	return document.getElementById(id).checked;
}

function setCheckbox(id, yes) {
	// Set the "checked" attribute of the given checkbox
	//
	document.getElementById(id).checked = yes;
}

function stopBubbling(event) {
	// general-purpose all-browser bubble prevention
	//
	event.cancelBubble = true;
	if (event.stopPropagation) event.stopPropagation();
}

function addMouseWheel(id, handler) {
	// Add a mouse wheel event handler to the named element.
	// The handler gets called with the wheel delta, normalized
	// to one notch = +1 or -1.
	//
	var elt = document.getElementById(id);
	var myHandler = function(event) {
			if (!event) event = window.event; // IE versus the rest
			var delta = (event.wheelDelta ? (event.wheelDelta / 120) :
				(-event.detail / 3));
			if (window.opera) delta = -delta;
			if (!handler(delta)) {
				if (event.preventDefault) event.preventDefault();
				return false;
			} else {
				return true;
			}
		};
	if (elt.addEventListener) {
		elt.addEventListener("DOMMouseScroll", myHandler, false);
		elt.addEventListener("mousewheel", myHandler, false);
	} else if (elt.attachEvent) {
		elt.attachEvent("onmousewheel", myHandler);
	}
}


//
// position calculations
//

function windowSize() {
	// return an object with the window's available width and height
	// With thanks to www.quirksmode.org
	//
	var size = Object();
	if (self.innerWidth) {
		size.x = self.innerWidth;
		size.y = self.innerHeight;
	} else if (document.documentElement &&
					document.documentElement.clientWidth) {
		size.x = document.documentElement.clientWidth;
		size.y = document.documentElement.clientHeight;
	} else if (document.body) {
		size.x = document.body.clientWidth;
		size.y = document.body.clientHeight;
	}
	return size;
}

function getElementPos(element) {
	// Return (x,y) for top-left of the given element, relative to document
	//
	var res = Object();
	res.x = 0;
	res.y = 0;
	for (var obj = element; obj.offsetParent; obj = obj.offsetParent) {
		res.x += obj.offsetLeft;
		res.y += obj.offsetTop;
	}
	return res;
}

function getMousePos(event) {
	// Return (x,y) for mouse coordinates, relative to the document
	// Thanks to quirksmode.org for browser-specific details.
	//
	var mouse = Object();
	if (event.pageX) {
		mouse.x = event.pageX;
		mouse.y = event.pageY;
	} else {
		mouse.x = event.clientX + document.body.scrollLeft +
			document.documentElement.scrollLeft;
		mouse.y = event.clientY + document.body.scrollTop +
			document.documentElement.scrollTop;
	}
	return mouse;
}

function moveToCenter(element, size) {
	// Move given element to center of given size
	//
	element.style.left = "" + Math.floor((size.x-element.offsetWidth)/2) +
		"px";
	element.style.top = "" + Math.floor((size.y-element.offsetHeight)/2) +
		"px";
}


//
// XMLHTTP access
//

// Use:
//   - create an object with the following attributes:
//       - "url"
//       - optional "postData", which implies method "POST" instead of "GET"
//       - optional "synchronous" boolean, defaulting to false
//       - method "handleFailure"
//       - method "handleResult"
//   - pass the object to "initiateXMLHttp"
//   - when request completes or fails, handleResult or handleFailure gets
//     called.  Exactly one of them will be called eventually.
//   - initiateXMLHttp2 and reqChange are private
//
// "get" is for backwards compatability only.

function initiateXMLHttp(thisReq) {
	// Fetch a URL via XMLHttpRequest machinery.
	//
	initiateXMLHttp2(thisReq, false);
}

function get(thisReq, url, postData, synchronous) {
	// Backwards compatibility.
	// Uses global "handleFailure" and "handleResult" functions
	//
	thisReq.url = url;
	thisReq.postData = postData;
	thisReq.synchronous = synchronous;
	thisReq.handleFailure = function() { handleFailure(this); };
	thisReq.handleResult = function(xmlhttp) {
			thisReq.xmlhttp = xmlhttp;
			handleResult(this);
		};
	initiateXMLHttp(thisReq);
}

function initiateXMLHttp2(thisReq, retrying) {
	// Internal subroutine for initiateXMLHttp, distinguishing retries
	//
	var xmlhttp = null;
	if (window.XMLHttpRequest) {
		xmlhttp = new XMLHttpRequest();
	} else if (window.ActiveXObject) {
		try {
			xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
		} catch(e) { }
	}
	if (xmlhttp) {
		var postData = thisReq.postData;
		var async = (thisReq.synchronous ? false : true);
		xmlhttp.onreadystatechange = function() {
				reqChange(xmlhttp, thisReq, retrying);
			};
		xmlhttp.open((postData ? "POST" : "GET"), thisReq.url, async);
		if (postData) xmlhttp.setRequestHeader("Content-type",
							"application/x-www-form-urlencoded");
		xmlhttp.send((postData ? postData : null));
	} else {
		alert("This browser has no known XMLHttp support");
		thisReq.handleFailure();
	}
}

function reqChange(xmlhttp, thisReq, retrying) {
	// Called on XMLHTTP state changes
	//
	if (xmlhttp.readyState == 4) {
		xmlhttp.onreadystatechange = function() { };
			// garbage collection assistance
		if (!xmlhttp.status || xmlhttp.status == 12029) {
			// Connection failures (Safari delivers null, IE gives 12029)
			// We retry once, after a short delay
			if (!retrying) {
				window.setTimeout(function() {
						initiateXMLHttp2(thisReq, true);
					},
					100);
			} else {
				thisReq.handleFailure();
			}
		} else if (xmlhttp.status == 200 || xmlhttp.status == 1) {
			thisReq.handleResult(xmlhttp);
		} else {
			thisReq.handleFailure();
		}
	}
}


//
// Parsing XML from a string
//

function parseXML(txt) {
  // Parse an XML sub-tree fragment, and return the DOM root node
  // I.e., res.documentElement is the top-level XML node
  //
  // Intended to work on any modern browser.
  if (window.DOMParser) {
    var parser = new window.DOMParser();
    return parser.parseFromString(txt, "text/xml");
  } else if (window.ActiveXObject) {
    try {
      var res = new ActiveXObject("Microsoft.XMLDOM");
      res.async = false;
      res.loadXML(txt);
      return res;
    } catch(e) { }
  }
  return null;
}
