/*
Handy dandy SassieX UI Object
@requires XO.js
@requires Error.js

@author bibby <bibby@surfmerchants.com>
$Id$
*/
var XUI=
{
	/*
	If you want show-hideable elements, pass them through XUI.visibilityToggle, and they'll learn how.
		XUI.visibilityToggle( obj );
		
	obj may now show(), hide(), showhide()
		"showhide" was chosen over "toggle" to avoid conflicts
	
	To begin in a hidden state, call hide() as soon as it is able (or better yet in your css):
		XUI.visibilityToggle( obj ).hide();
	
	elements may also control the visibility of objects other than themselves by supplying the controlling object as an argument.
		XUI.visibilityToggle( obj_tar , obj_ctrl)
	
	obj_tar now has show(), hide(), showhide().
	obj_ctrl  can toggle obj_tar with _show() , _hide(), _showhide();
	
	The reason for the underscores in the naming is to allow foo to also control
	itself (or be controlled by something else) without conflict. Controllers aren't given "hide me" functions, though.
	
	so, no underscore == toggle this
	with underscore == toggle bound element
	
	@param object target to show hide
	@param object (optional) object to control the target
	*/
	visibilityToggle:function(target, controller)
	{
		if(!is(target,'object'))
			return Error.IllegalArgument(new Error(), 'visibilityToggle HTML Element', target);
		
		if( is(controller,'object') )
		{
			XO.x(controller ,
			{
				_showhideTarget:target,
				_showhide:function()
				{
					this._showhideTarget.showhide();
					return this;
				},
				_show:function()
				{
					this._showhideTarget.showhide(false);
					return this;
				},
				_hide:function()
				{
					this._showhideTarget.showhide(true);
					return this;
				}
			});
		}
		
		XO.x(target,
		{
			hidden:false,
			showhide:function(hidden)
			{
				if(is(hidden,'boolean'))
					this.hidden=hidden;
				else
					this.hidden = !this.hidden; // toggle
				
				if(!is(this.style))
					Error.IllegalProcedure(new Error(),' visibilityToggle not yet bound to an element (that has .style)');
				else
					this.style.display = this.hidden? 'none' : '';
				return this;
			},
			
			show:function()
			{
				this.showhide(false);
				return this;
			},
			
			hide:function()
			{
				this.showhide(true);
				return this;
			}
		});
		
		return target;
	},
	
	/**
	Determines if the three arguments are of the proper types 
	@access private (by intent)
	@param string event type
	@param object event target
	@param function event action
	@return boolean
	*/
	_validEvent:function(type, obj, fn)
	{
		if(!is(obj,'object'))
			return Error.IllegalArgument( new Error() , 'object' , obj);
		
		if(!is(type,'string'))
			return Error.IllegalArgument( new Error() , 'string' , type);
		
		if(!is(fn,'function'))
			return Error.IllegalArgument( new Error() , 'function' , fn);
		
//		if( ![ *list of event types?* ].in(type.toLowerCase))
//			return Error.IllegalArgument( new Error() , 'valid event type ("click", not "onclick")' , type);

		return true;
	},
	
	/*
	var to store which event model we are to use (Mozilla vs Microsoft deathmatch)
	*/
	eventModel:null,
	
	/*
	Creates a test object to test for event methods, and
	sets this.eventModel according to what it found.
	@return int representing a model
	*/
	getEventModel:function()
	{
		var sp = document.createElement('SPAN');
		this.eventModel=0;
		if(sp.addEventListener)
			this.eventModel = 1; // FF
		else if(sp.attachEvent)
			this.eventModel = 2; // IE
		else
			this.eventModel = -1; // ??
		
		XUI.getEventModel = function(){ return this.eventModel; };
		
		return this.eventModel;
	},
	
	/*
	Events may be bubbling (and certainly are with IE).
	This method returns the highest level "true target", the smallest bubble.
	This function is used as a workaround to IE's using the "this" keyword as window in
	attached Events.
	@param event
	@return object
	*/
	getEventTarget:function(e)
	{
		var targ;
		if (!e) var e = window.event;
		if (e.target) targ = e.target;
		else if (e.srcElement) targ = e.srcElement;
		if (targ.nodeType == 3) // defeat Safari bug
			targ = targ.parentNode;
		
		return targ;
	},
	
	/**
	Add an event listener to the object.
	Unlike the native addEventListener (which is still used here), 
	we're going to register the event in a member array in the object.
		obj.events ->  .click , .mouseover , etc
	That way, we have easy access to it (especially when removing);
	
	More importantly, the registered event executes the function stored in .events, 
	so it can be changed on the fly!
	
	@param string event type
	@param object event target
	@param function event action
	@param boolean Execute during capture phase (true) or bubble phase (false)
	@return EventStub
	*/
	addEvent:function(type, obj, fn, capture)
	{
		if(is(this.eventModel,'null'))
			this.getEventModel();
		
		if(this._validEvent(type, obj, fn) !== true)
			return false;
		
		type = type.toLowerCase();
		
		if(!is(obj.events,'object'))
			obj.events = {};
		if(!is(obj.events[type],'array'))
			obj.events[type] = [];
		
		var eventIndex = obj.events[type].push([fn,capture]) - 1;
		var eventFn = function(obj,type,eventIndex,e)
		{
			obj = XUI.getEventTarget(e);
			
			if(obj && obj.events && obj.events[type] && obj.events[type][eventIndex])
				obj.events[type][eventIndex][0].call(obj,e);
			else if((is(obj.parentNode)))
			{
				do
				{
					obj = obj.parentNode;
					if(obj && obj.events && obj.events[type] && obj.events[type][eventIndex])
					{
						obj.events[type][eventIndex][0].call(obj,e);
						obj=false;
					}
				}
				while(is(obj.parentNode))
			}
		};
		
		switch(this.eventModel)
		{
			case 1:
				obj.addEventListener(type , (function(e){ eventFn(this,type,eventIndex,e)}) , capture);
				break;
			case 2:
				obj.attachEvent('on'+type, (function(e){ eventFn(this,type,eventIndex,e)}) );
				break;
		}
		
		var stub = {'type':type , index:eventIndex };
		
		if(is(obj.id))
			stub.id = obj.id;
		else if(is(obj.name))
			stub.name = obj.name;
		else
			stub.obj = obj;
			
		return new XUI.EventStub(stub);
	},
	
	
	/**
	A constructor for stubs,
	packets that are returned by registering events.
	Should you need to reference an Event for replacing or removing, please keep your stub.
	@access private (but needs to be a member of XUI so that we can check instanceof)
	@param
	@return object instanceof XUI.EventStub
	*/
	EventStub:function(stub)
	{
		XO.x(this,stub);
	},
	
	
	/**
	Once you've registered for event, you might want to know where it went.
	This function returns what is stored in .events with just an object an function
	
	"type" and "index" corelate to obj.events.  If type were 'click' and index 1 , you'll get:
	obj.events.click[0] ( == [fn,capture] ) 
	
	@access public
	@param object
	@param function
	@return array of stored event vars  [ type , fn , capture, index]
	*/
	getEvent:function(obj , fn)
	{
		if(!is(obj,'object'))
			return Error.IllegalArgument( new Error() , 'object' , obj);
		
		if(obj instanceof XUI.EventStub)
			return this.getStubEvent(obj);
		
		if(!is(fn,'function'))
			return Error.IllegalArgument( new Error() , 'function' , fn);
		
		if(!is(obj.events,'object'))
			return false;
		
		var ret = false;
		for(var t in obj.events)
		{
			obj.events[t].each(function(i)
			{
				if(this[0] == fn)
				{
					// [type, fn, capture, index?]
					ret = {
						type:t,
						fn:this[0],
						capture:this[1],
						index:i
					};
				}
			});
		}
		
		return ret;
	},
	
	/**
	gets an Event from an EventStub
	@access private
	@param
	*/
	getStubEvent:function(stub)
	{
		var obj = false;
		if(stub.id)
			obj = document.getElementById(stub.id);
		else if(stub.name)
		{
			var names = XUtil.toArray(document.getElementsByName(stub.name));
			if(names.length == 0)
				return Error.IllegalArgument(new Error() , 'HTMLElement matching stub.name', stub.name);
			obj = names[0];
		}
		else if(is(stub.obj,'object'))
			obj = stub.obj;
		
		if(!is(obj.events) || !is(obj.events[stub.type]) || !is(obj.events[stub.type][stub.index]))
			return Error.IllegalArgument(new Error() , 'HTMLElement with event matching stub', obj);
		
		var e = obj.events[stub.type][stub.index];
		
		return {
			type:stub.type,
			fn:e[0],
			capture:e[1],
			index:stub.index,
			obj:obj
		};
	},
	
	
	/**
	Dropping an event listener means remembering the object, function, type, and capture phase.
	This function lets you skip two and drop by object and function.
	I *hope* you kept a reference to that fn ;)
	getEvent() returns the missing pieces
	
	@param object
	@param function
	@return boolean
	*/
	dropEvent:function(obj, fn)
	{
		var e = this.getEvent(obj,fn);
		if(is(e,'object'))
		{
			if(is(e.obj,'object'))
			{
				obj=e.obj;
			}
			return this._dropEvent( obj, e);
		}
		
		return false;
	},
	
	
	/**
	The interal function to finally drop the event from the object.
	@access private (by intent)
	@param object
	@param array of stored event vars  [ type , fn , capture, index] 
	*/
	_dropEvent:function(obj, e)
	{
		if(this._validEvent(e.type, obj, e.fn) !== true)
			return false;
		
		if(obj['obj'])
		{
			e = new XO(obj);
			obj = e['obj'];
		}
		
		
		switch(this.eventModel)
		{
			case 1:
				obj.removeEventListener(e.type, e.fn, e.capture);
				break;
			case 2:
				obj.detachEvent('on'+e.type, e.fn);
			break;
		}
		
		delete obj.events[e.type][e.index];
		return true;
	},
	
	
	/**
	Removes event listeners 
	@param object
	@param string type to clear ( !Warning! giving no type clears all events!)
	*/
	clearEvents:function(obj,type)
	{
		if(!is(obj,'object'))
			return Error.IllegalArgument( new Error() , 'object' , obj);
		if(is(type) && !is(type,'string'))
			return Error.IllegalArgument( new Error() , 'string' , type);
		
		if(!is(obj.events,'object'))
			return;
		
		for(var t in obj.events)
		{
			if(is(type) && t != type.toLowerCase())
				continue;
			
			obj.events[t].each(function()
			{
				XUI.dropEvent(t,obj,this[0],this[1]);
			});
		}
	},
	
	/**
	Replace one event with another
	@param object
	@param function Old function
	@param function New function
	@param boolean , capture|bubble phase. If omitted, stays the same
	*/
	replaceEvent:function(obj, oldFn, fn, capture)
	{
		//work arounds for stubs for now.
		if(obj instanceof XUI.EventStub)
		{
			var stub = this.getStubEvent(obj);
			fn = oldFn;
			obj = stub.obj;
			
			obj.events[stub.type][stub.index]=[fn,stub.capture];
			return;
		}
		
		
		if(!this._validEvent('foo', obj, fn))
			return false;
		if(!is(oldFn,'function'))
			return Error.IllegalArgument( new Error() , '"old" function' , oldFn);
		
		var e = this.getEvent(obj,oldFn);
		if(!is(e,'array'))
			return false;
		
		obj.events[e[0]][e[3]] =[ fn , (is(capture,'boolean') ? capture : e[2])] ;
		return true;
	},
	
	
	/**
	wax on , wax off, only with the mouse.
	Applies two event listeners for rollovers
	@param object
	@param function mouseover
	@param function mouseout
	*/
	hover:function(obj, onFn, offFn)
	{
		if(!is(obj, 'object'))
				return Error.IllegalArgument( new Error , 'object' , obj);
		if(!is(onFn, 'function'))
				return Error.IllegalArgument( new Error , 'function (onFn)' , onFn);
		if(!is(offFn, 'function'))
				return Error.IllegalArgument( new Error , 'function (offFn)' , offFn);
		
		// two EventStubs
		return [this.mouseover( obj, onFn ) , this.mouseout( obj, offFn )];
	},
	
	
	// ### EVENT TYPES ### //
	// see http://www.quirksmode.org/dom/events/ for compatibility comparisons
	
	/** register a blur event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	blur:function(obj, fn, capture) 
	{
		return this.addEvent('blur', obj , fn , !!capture);
	},
	
	
	/** register a change event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	change:function(obj, fn, capture) 
	{
		return this.addEvent('change', obj , fn , !!capture);
	},
	
	
	/** register a click event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	click:function(obj, fn, capture) 
	{
		return this.addEvent('click', obj , fn , !!capture);
	},
	
	
	/** register a contextmenu event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	contextmenu:function(obj, fn, capture) 
	{
		return this.addEvent('contextmenu', obj , fn , !!capture);
	},
	
	
	/** register a copy event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	copy:function(obj, fn, capture) 
	{
		return this.addEvent('copy', obj , fn , !!capture);
	},
	
	
	/** register a cut event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	cut:function(obj, fn, capture) 
	{
		return this.addEvent('cut', obj , fn , !!capture);
	},
	
	
	/** register a dblclick event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	dblclick:function(obj, fn, capture) 
	{
		return this.addEvent('dblclick', obj , fn , !!capture);
	},
	
	
	/** register a focus event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	focus:function(obj, fn, capture) 
	{
		return this.addEvent('focus', obj , fn , !!capture);
	},
	
	
	/** register a keydown event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	keydown:function(obj, fn, capture) 
	{
		return this.addEvent('keydown', obj , fn , !!capture);
	},
	
	
	/** register a keypress event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	keypress:function(obj, fn, capture) 
	{
		return this.addEvent('keypress', obj , fn , !!capture);
	},
	
	
	/** register a keyup event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	keyup:function(obj, fn, capture) 
	{
		return this.addEvent('keyup', obj , fn , !!capture);
	},
	
		/** get a key from an onkey event
	@param event
	*/
	getKey:function(e) 
	{
		var NS = (window.Event) ? 1 : 0;
		var evt = (NS) ? e : event;
		var code = (NS) ? e.which : event.keyCode;
		var key = String.fromCharCode(code);
		
		return key;
	},
	
	/** register a mousedown event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	mousedown:function(obj, fn, capture) 
	{
		return this.addEvent('mousedown', obj , fn , !!capture);
	},
	
	
	/** register a mousemove event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	mousemove:function(obj, fn, capture) 
	{
		return this.addEvent('mousemove', obj , fn , !!capture);
	},
	
	
	/** register a mouseout event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	mouseout:function(obj, fn, capture) 
	{
		return this.addEvent('mouseout', obj , fn , !!capture);
	},
	
	
	/** register a mouseover event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	mouseover:function(obj, fn, capture) 
	{
		return this.addEvent('mouseover', obj , fn , !!capture);
	},
	
	
	/** register a mouseup event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	mouseup:function(obj, fn, capture) 
	{
		return this.addEvent('mouseup', obj , fn , !!capture);
	},
	
	
	/** register a mousewheel event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	mousewheel:function(obj, fn, capture) 
	{
		return this.addEvent('mousewheel', obj , fn , !!capture);
	},
	
	
	/** register a paste event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	paste:function(obj, fn, capture) 
	{
		return this.addEvent('paste', obj , fn , !!capture);
	},
	
	
	/** register a reset event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	reset:function(obj, fn, capture) 
	{
		return this.addEvent('reset', obj , fn , !!capture);
	},
	
	
	/** register a resize event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	resize:function(obj, fn, capture) 
	{
		return this.addEvent('resize', obj , fn , !!capture);
	},
	
	
	/** register a scroll event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	scroll:function(obj, fn, capture) 
	{
		return this.addEvent('scroll', obj , fn , !!capture);
	},
	
	
	/** register a select event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	select:function(obj, fn, capture) 
	{
		return this.addEvent('select', obj , fn , !!capture);
	},
	
	
	/** register a submit event to an object. 
	@param object 
	@param function 
	@param boolean Capture phase (true) or bubble phase (false). Default is false 
	*/
	submit:function(obj, fn, capture) 
	{
		return this.addEvent('submit', obj , fn , !!capture);
	},
	
	
	// ### /EVENTS //
	
	// ## CSS
	
	/*
	obj.style is only aware of style rules set by javascript, and not those set inline or by style sheets.
	This method attempts to get the fully rendered "computed style" of the object - the real style.
! not fully tested for IE (obj.currentStyle)
	@param object
	@return object property:value pairs
	*/
	getCSS:function(obj)
	{
		if(!is(obj,'object'))
			return Error.IllegalArgument( new Error() , 'HTML Object' , obj);
		
		if(document.defaultView && document.defaultView.getComputedStyle)
			return document.defaultView.getComputedStyle(obj,'');
		else if(obj.currentStyle)
		{
			return obj.currentStyle;
		}
	},
	
	/*
	Get or Set a css rule.
	First param is the object.
	If second param is a string, say 'color', the color value will be returned from the real computed style.
	If second param is an object of property:value's , these will be set into the style.
! not fully tested for IE (obj.currentStyle)
	@param object
	@param mixed (string gets, object sets)
	@return object
	*/
	css:function(obj , getset)
	{
		if(!is(obj,'object') || !is(obj.style))
			return Error.IllegalArgument(new Error() , 'HTML Object' , obj);
		
		if(!is(getset))
			return false;
		
		if(is(getset,'string'))
		{
			var css = this.getCSS(obj);
			return is(css[getset]) ? css[getset] : '';
		};
		
		if(is(getset,'object'))
		{
			for(var css in getset)
				if(css in obj.style)
					obj.style[css] = getset[css];
				else if(css == 'className' || css =='class')
					obj.className = getset[css];
		}
		
		return obj;
	}

};

