// ------------------------------------------------------------------------------------------------------------------
/*
Browser independent helper functions
(c) 2007 Rob Watkins
This code is free to use for any purpose, provided the copyright notice and credit to original author is maintained.
The code is provided AS IS. No guarantee is made on the fitness of this code for any purpose. Use at your own risk.
The documentation must be kept with this source on redistibution.
*/

// GLOBAL FUNCTIONS NOT IN A NAMESPACE FOR BREVITY ------------------------------
function INT(o, def)
{
	def = !isNaN(parseInt(def)) ? parseInt(def) : 0;
	o = parseInt(o);
	return !isNaN(o) ? o : def;
}	
// ------------------------------------------------------------------------------

var Core = 
{
	createNamespace:function(newnsname, existingnamespace)
	{
		existingnamespace = existingnamespace ? existingnamespace : HS;
		
		if(existingnamespace && (typeof(existingnamespace[newnsname]) == "undefined"))
			existingnamespace[newnsname] = {};
	}
};

var HS = {};
var Helmstone = HS;

var Utils =
{
	go:function(url)
	{
		window.location = url;
	},
	
	get:function(ido)
	{
		if(typeof ido == 'string')
			return document.getElementById && (o = document.getElementById(ido)) ? o : null;
		else
			return ido;
	},

	// Get all elements of a given class. 
	getElementsByClassName:function(classname, root, nodename)
	{
		return this.find(root, nodename, classname, '*');
	},
	
	// Based on getElementsByClassNAme by Jonathan Snook, http://www.snook.ca/jonathan, add-ons by Robert Nyman, http://www.robertnyman.com, 
	// Rewritten for added functionality by me
	find:function(root, nodename, classname, id)
	{
		// If doing a global search only by ID, use Utils.get() as will be quicker
		if(id && !root && !nodename && !classname)
			return this.get(id);
			
		root = root ? root : document;
		root = this.get(root);
		nodename = nodename ? nodename : '*';
		classname = classname ? classname : '*';
		id = id ? id : '*';
		
		var els = (nodename == '*' && root.all) ? root.all : root.getElementsByTagName(nodename);		
		var res = [];
		
		var el;
		
		for(var i = 0; i < els.length; i++)
		{
			el = els[i];
						
			if( ((id == '*') || (el.id == id)) && ((classname == '*') || this.hasClass(el, classname)) )
				res.push(el);
		}
		
		return res;
	}, 
	
	hide:function(ido, removefromflow) 
	{ 
		if(removefromflow)
		{
			var o = Utils.get(ido);
			if( o && ((o.style.display != 'none') || !o.style.display)) // Doesn't seem to pick up initial style, so also run if blank so it works on first call
			{
				o.__prevDisplay__ = o.style.display;
				this.safeSetStyle(ido, 'display', 'none'); 
			}
		}
		else
			this.safeSetStyle(ido, 'visibility', 'hidden'); 
	},
	
	show:function(ido, replaceinflow, inline) 
	{ 
		if(replaceinflow)
		{
			var o = Utils.get(ido);
			
			if( o && ((o.style.display == 'none') || !o.style.display)) // Doesn't seem to pick up initial style, so also run if blank so it works on first call
			{
				var disp = o.__prevDisplay__ ? o.__prevDisplay__ : (inline ? 'inline' : 'block');
				this.safeSetStyle(ido, 'display', disp); 
			}
		}
		else			
			this.safeSetStyle(ido, 'visibility', 'visible'); 
	},
	
	showHide:function(ido, visible, changeflow)
	{
		if(visible)
			this.show(ido, changeflow);
		else
			this.hide(ido, changeflow);
	},
	
	showToggle:function(ido, changeflow)
	{	
		ido = this.get(ido);
		if(ido)
		{
			var visible = changeflow ? ido.style.display != 'none' : ido.style.visibility != 'hidden';
			this.showHide(ido, !visible, changeflow);
			
			return !visible;
		}
	},
	
	// If the passed in object has the specified class, returns that object. Otherwise goes back up the HTML
	// containing elements until it finds that class, and returns that element
	getNearest:function(ido, classname)
	{
		var o = this.get(ido);
		
		while(o && o.parentNode && !this.hasClass(o, classname))
			o = o.parentNode;
			
		return o;
	},
	
	// Returns true if child is any descendant of parent
	isDescendant:function(child, parent)
	{
		var o = this.get(child);
		parent = this.get(parent);
		
		if(o && parent)
			for(o = o.parentNode;o;o = o.parentNode)
				if(o == parent)
					return true;
					
		return false;
	},
	
	// Return actual classname of an object, or 'null'
	getType:function(o)
	{
		if(o != null)
		{		
			if(o.__typeName) 
				return o.__typeName;
			else
			{
				/^\s*function ([\w_]+[\w\d_]*)/.exec(o.constructor);
				return RegExp.$1;
			}
		}
		else
			return 'null';
	},
	
	// Add extra properties to an object. It's up to you to ensure names don't clash.
	addProperties:function(o, props)
	{
		for(var el in props)
			o[el] = props[el];
	},
	
	iterate:function(a, f, param)
	{
		if(a != null)
			for(var i = 0; i < a.length; i++)
				f(a[i], i, param);
	},
	
	// Set a style on an object, if it exists
	safeSetStyle:function(ido, prop, value)
	{
		(ido = this.get(ido)) && (ido.style[prop] = value);
	},

	// Set an arbitrary proprty on an object, if it exists
	safeSetProperty:function(ido, prop, value)
	{
		(ido = this.get(ido)) && (ido[prop] = value);
	},
	
	// Does the element have the specified class?
	hasClass:function(ido, name)
	{
		ido = this.get(ido);
		name = name.replace(/\-/g, '\\-');

		var re = new RegExp('(^|\\s)' + name + '(\\s|$)');

		return ido && re.test(ido.className);
	},
	
	// Add a classname to an object. This and removeClass VERY useful for changing object look and feel in Javascript
	addClass:function(ido, name)
	{
		if(ido = this.get(ido))
			if(this.inArray(name, this.getClasses(ido)) < 0)
				ido.className += " " + name;
	},

	// Remove classname from an object. This and addClass VERY useful for changing object look and feel in Javascript
	removeClass:function(ido, name)
	{
		var a, idx;
		if(ido = this.get(ido))
		{
			a = this.getClasses(ido);
			idx = this.inArray(name, a);
			
			if(idx >= 0)
				a.splice(idx, 1);
				
			ido.className = a.join(" ");
		}
	},
	
	// Flip between classes; utility function, useful for UI state changing
	flipClass:function(ido, ison, classname)
	{
		if(ison)
			this.addClass(ido, classname);
		else
			this.removeClass(ido, classname);
	},
	
	// Get all classes applied to an object as an array
	getClasses:function(ido)
	{
		var a = [];
		ido = this.get(ido);
		
		if(ido)
			a = ido.className.split(/\s+/);
		
		return a;
	},
	
	// Roughly equivalent to using object.innerText, only standards compliant
	createElementText:function(ido, text)
	{
		if(ido = this.get(ido))
		{		
			var elt = document.createTextNode(text);
			ido.appendChild(elt);
		}
	},
	
	// Empty a node
	clearChildren:function(node)
	{
		while(node.firstChild)
			node.removeChild(node.firstChild);
	},	

	deleteNode:function(node)
	{
		var p = node.parentNode ? node.parentNode : document;
		this.clearChildren(node);
				
		p.removeChild(node);
	},	

	// Find the absolute screen rendered position of an element. Returns an object with x and y properties
	// Useful for popping up dialogs / tooltips regardless of scroll state
	findPos:function(ido) 
	{
		if(ido = this.get(ido))
		{				
			var curleft = curtop = 0;
			
			if(ido.offsetParent) 
			{
				curleft = ido.offsetLeft;
				curtop = ido.offsetTop;
				
				while(ido = ido.offsetParent)
				{
					curleft += ido.offsetLeft;
					curtop += ido.offsetTop;
				}
			}
			
			var ie = window.getComputedStyle ? 0 : 1; // No idea why you need to do this in IE, and can't find anything on the web about it.
			return {x:curleft + ie, y:curtop + ie};
		}
		else
			return null
	},
	
	// Find absolute rendered size and current visibility of an object. Returns an object with w, h, and visible properties
	// N.B!!! USE MATCHSIZE to set an element to match another one, as it accounts for box model differences
	findDim:function(ido)
	{
		if(ido = this.get(ido))
		{
			var res ={w:ido.offsetWidth, h:ido.offsetHeight, visible:((ido.style.visibility != 'hidden') && (ido.style.display != 'none')) };
			return res;
		}
		else
			return null;
	},
	
	getStyles:function(ido, stylenames)
	{
		if(ido = this.get(ido))
		{
			var res = {};
			
			for(var i = 0; i < stylenames.length; i++)
			{
				var style = stylenames[i];
				
				if(ido.currentStyle && ido.currentStyle[style])
					res[style] = ido.currentStyle[style];
				else if (window.getComputedStyle)
				{
					var o = document.defaultView.getComputedStyle(ido, null);
					o = o ? o.getPropertyValue(style) : null;
					
					if(o)
						res[style] = o;
				}
			}
			
			return res;
		}
		else
			return null;
	},	
	
	// Set the width and / or height of an element to match another, taking into account box model retardedness
	// If exact is true, then width / height will be set; if false, and browser supports minHeight / minWidth, they will be used
	matchSize:function(src, dest, matchw, matchh, exact)
	{
		var a = ['padding-top', 'padding-bottom', 'padding-left', 'padding-right', 'border-top-width', 'border-bottom-width', 'border-left-width', 'border-right-width', 
							'min-height', 'min-width'];
		var dim = this.findDim(src);
		var ds = this.getStyles(dest, a);

		var hasmin = typeof(ds['min-height']) != 'undefined';
		
		var w = dim.w;
		var h = dim.h;
		
		// W3C box model compensations - we don't need to do this in IE, so it is convenient that the IE style names are different
		dest = this.get(dest);
		if(matchw)
		{
			w -= (INT(ds['padding-left']) + INT(ds['padding-right']) + INT(ds['border-left-width']) + INT(ds['border-right-width']));
			
			if(hasmin && !exact)
				dest.style.minWidth = w;
			else
				dest.style.width = w;
		}
			
		if(matchh)
		{
			h -= (INT(ds['padding-top']) + INT(ds['padding-bottom']) + INT(ds['border-top-width']) + INT(ds['border-bottom-width']));
			
			if(hasmin && !exact)
				dest.style.minHeight = h;
			else
				dest.style.height = h;
		}		
	},
	
	// Find cursor position from an Event object
	findCursorPos:function(e)
	{
		e = Evt.get(e);
		var x, y;
		
		// Get cursor position with respect to the page.
		if(window.scrollX) 
		{
			x = e.clientX + window.scrollX;
			y = e.clientY + window.scrollY;
		}
		else if(e.pageX)
		{
			x = e.pageX;
			y = e.pageY;
		}
		else
		{
			x = e.clientX + document.documentElement.scrollLeft + document.body.scrollLeft;
			y = e.clientY + document.documentElement.scrollTop + document.body.scrollTop;
		}
		
		return {x:x, y:y};
	},
	
	// Given absolute screen coordinates, returns coordinates that are relative to the object passed in.
	// This will break if the object is moved, obviously - for example on a centred object if the page is resized
	screenToClient:function(ido, a_pt)
	{
		if(ido = this.get(ido))
		{
			var r_pt = this.findPos(ido);
			return {x:a_pt.x - r_pt.x, y:a_pt.y - r_pt.y};
		}
		else
			return null;
	},
	
	limit:function(i, min, max)
	{
		return i < min ? min : (i > max ? max : i);
	},
	
	parseRect:function(rect)
	{
		var t, r, b, l;
		var re = /rect\(\s*([^\s,]+)[\s,]+([^\s,]+)[\s,]+([^\s,]+)[\s,]+([^\s,]+)\s*\)/;
		
		if(re.exec(rect))
		{
			t = parseFloat(RegExp.$1);
			r = parseFloat(RegExp.$2);
			b = parseFloat(RegExp.$3);
			l = parseFloat(RegExp.$4);
			
			if(!isNaN(t) && !isNaN(r) &&!isNaN(b) &&!isNaN(l))
				return { t:t, r:r, b:b, l:l };
		}
		
		return null;
	},
	
	objectToJSON:function(o)
	{	
		var s = "{ ";
		for(el in o)	
			s += el + ":" + o[el] + ", ";
		
		return s == "{ " ? "{}" : s.substring(0, s.length - 2) + " }";
	},
	
	// Use like .NET String.Format()
	formatString:function(s, args)
	{
		if(args)
			for(var i = 0; i < args.length; i++)
				s = s.replace(new RegExp('\\{' + i + '\\}', 'g'), args[i]);
			
		return s;
	},
	
	trim:function(s)
	{
		//                                 Trim start           Trim end
		return s != null ? new String(s).replace(/^\s+/, '').replace(/\s+$/, '') : null;
	},

	// Similar to formatString but replaces AJAX / SQL style @var with values from an object
	replaceParams:function(s, obj)
	{
		for(var el in obj)
			s = s.replace(new RegExp('@' + el, 'g'), obj[el]);
			
		return s;
	},
	
	makeUrl:function(url, qs)
	{
		if(url != null)
			return url + (url.indexOf('?') < 0 ? '?' : '&') + qs;
		else
				return null;
	},

	// Find object in an array
	// To find an object based on a property of that object, use arrayExtend() and call find()
	inArray:function(o, a)
	{
		var i;
		
		for(i = 0; i < a.length; i++)
			if(a[i] == o)	
				return i;
				
		return -1;
	},
	
	// All the extended methods work within the current array, not on a returned new array
	arrayExtend:function(a)
	{
		if(a != null)
		{
			a.find = function(key, property) { for(var i = 0; i < this.length; i++) if((property ? this[i][property] : this[i]) == key) return i; return -1; };
			a.update = function(key, val, property) { var i = this.find(key, property); if(i >= 0) this[i] = val; else this[this.length] = val; return i; };
			a.remove = function(key, property) { this.removeAt(this.find(key, property)); }
			a.removeAt = function(index) 
			{ 
				var shift = false;
				var stop = this.length;
				
				for(var i = 0; i < stop; i++)
				{
					if(index == i)
					{
						shift = true;
						stop--;
					}
						
					this[i] = shift ? this[i + 1] : this[i];
				}
				
				this.length--;
			};					
			a.joinObjects = function(sep, field)
			{
				var _a = [];
				for(var i = 0; i < this.length; i++)
					_a[i] = this[i][field];
					
				return _a.join(sep);
			};			
			a.coalesce = function(sep, field) // Coalesces current array, AND return string representation.
			{
				for(var i = 0; i < this.length; i++)
					if(this[i] == null)
						this.removeAt(i--);
						
				return field ? this.joinObjects(sep, field) : this.join(sep);
			};
		}
	
		return a;
	},
				
	// Returns an array with the item removed, does NOT affect original array
	arrayDelete:function(a, index)
	{
		var res = [];
		for(var i = 0; i < a.length; i++)
			if(i != index)
				res[res.length] = a[i];
				
		return res;
	},
	
	id:function(prefix)
	{
		return (prefix ? prefix : '_ctrl_') + (nextID++);
	},
	
	nextID:0
};

// BROWSER INDEPENDENT EVENT UTILS
var Evt =
{	
	get:function(e)
	{
		return e ? e : window.event;
	},
	
	// Get event source element
	sender:function(e)
	{
		e = Evt.get(e);
		
		var res;
				
		if(e.target)	
			res = e.target;
		else if(e.srcElement)
			res = e.srcElement;
			
		if(res.nodeType == 3) // defeat Safari bug
			res = res.parentNode;
			
		return res;
	},
	
	// Swallow an event - i.e. stop it propagating
	swallow:function(e, allowdefault) 
	{
		e = Evt.get(e);
		
		e.cancelBubble = true;
		e.returnValue = allowdefault ? true : false;
		
		if(e.stopPropagation)
			e.stopPropagation();
	
		if(e.preventDefault && !allowdefault)
			e.preventDefault();
			
			return false;
	},
	
	attachLoadHandler:function(handler)
	{
		this.attach('load', handler, window, false);
	},

	onLoad:function(handler) // Shortcut
	{
		this.attachLoadHandler(handler);
	},
	
	// Attach an event to an object - omit the "on"; so onmousedown will be "mousedown" etc.
	// DEPRECATED: use Evt.trackEvent()
	attach:function(eventname, handler, ido, capture)
	{
		ido = Utils.get(ido);
		if(ido)
		{
			if(ido.attachEvent) 
				ido.attachEvent('on' + eventname, handler); 
			else if(ido.addEventListener)
				ido.addEventListener(eventname, handler, capture);	
		}
	},
	
	// Detach an event from an object - omit the "on"; so onmousedown will be "mousedown" etc.
	detach:function(eventname, handler, ido, capture)
	{
		ido = Utils.get(ido);
		if(ido)
		{
			if(ido.detachEvent) 
				ido.detachEvent('on' + eventname, handler); 
			else if(ido.removeEventListener)
				ido.removeEventListener(eventname, handler, capture);	
		}
	},
	
	trackEvent:function(eventname, handler, ido, options)
	{
		return new Evt.EventTracker(eventname, handler, ido, options);
	},
	
	EventTracker:function(eventname, handler, ido, options)
	{
		// -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  
		this._handler = function(e) { handler(Utils.get(e), options.id, options.data) };
		
		// -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  
		this.detach = function()
		{
			Evt.detach(this.event, this._handler, this.element, this.options.capture)
		}
		// -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  
		
		ido = Utils.get(ido);
		if(ido)
		{
			options = options ? options : {};
			options.id = ido.id;
			options.data = options.data ? options.data : {};
			options.capture = options.capture ? options.capture : false;
			
			Evt.attach(eventname, this._handler, ido, options.capture);
			
			this.options = options;
			this.event = eventname;
			this.element = ido;
		}
	},
	
	trackMouse:function(ido, options)
	{
		return new Evt.MouseTracker(ido, options);
	},
	
	MouseTracker:function(ido, options)
	{
		// -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  
		this._move = function(e) { options.mouseMove(Evt.get(e), options.id, Utils.findCursorPos(e), options.data) };
		this._over = function(e) { options.mouseOver(Evt.get(e), options.id, Utils.findCursorPos(e), options.data) };
		this._out = function(e) { options.mouseOut(Evt.get(e), options.id, Utils.findCursorPos(e), options.data) };
		this._down = function(e) { options.mouseDown(Evt.get(e), options.id, Utils.findCursorPos(e), options.data) };
		this._up = function(e) { options.mouseUp(Evt.get(e), options.id, Utils.findCursorPos(e), options.data) };
		this._click = function(e) { options.click(Evt.get(e), options.id, Utils.findCursorPos(e), options.data) };
		this._dblclick = function(e) { options.dblClick(Evt.get(e), options.id, Utils.findCursorPos(e), options.data) };
				
		// -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  
		this.detach = function(events)
		{			
			events = events ? events : this.events;// If none provided, detach all
			
			for(var i = 0; i < events.length; i++)
			{
				var evt = events[i].toLowerCase();
				Evt.detach(evt, this.handlers[evt].handler, this.element, this.handlers[evt].capture);
			}
		};
		// -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  
		ido = Utils.get(ido);
		if(ido)
		{			
			options = options ? options : {};
			options.id = ido.id;
			options.data = options.data ? options.data : {};
			
			this.handlers ={	mousemove: { handler:this._move,     capture: typeof(options.captureMove) == 'undefined' ? true : options.captureMove },
												mouseover: { handler:this._over,     capture: typeof(options.captureOver) == 'undefined' ? true : options.captureOver },
												mouseout:  { handler:this._out,      capture: typeof(options.captureOut) == 'undefined' ? true : options.captureOut },
												mousedown: { handler:this._down,     capture: options.captureDown },
												mouseup:   { handler:this._up,       capture: options.captureUp },
												click:     { handler:this._click,    capture: options.captureClick },
												dblclick:  { handler:this._dblclick, capture: options.captureDblClick } };
																										
			this.events = [];
			for(var el in options)
			{
				var evt = el.toLowerCase();
								
				if(this.handlers[evt])
				{
					Evt.attach(evt, this.handlers[evt].handler, ido, this.handlers[evt].capture);
					this.events[this.events.length] = evt;
				}
			}
			
			this.options = options;
			this.element = ido;
		}
	}
};

var DEBUG = 
{
	write:function(s)
	{
		var o;
		if(o = Utils.get('_DEBUG_'))
			o.innerHTML += s + '<br>';
	}
}