/** 
 * @fileoverview Document Object Model-related functionality for the JSPOOP library
 * @author James Palmer james@phoenixlondon.co.uk
 * @version 0.1 
 */

/**
 * Construct the Phoenix.DOM object.
 * @class Base class for DOM-related functionality in the Phoenix library.
 * @constructor
 * @requires Phoenix
 */
Phoenix.DOM = function()
{
};
//registerNamespace("Phoenix.DOM");


/**
 * Return an array of all elements with the CSS class "searchclass".
 * 
 * @param		{String}		searchclass	A string representing the CSS class to search for
 * @param		{object}		node				Optional: Restrict the elements returned to only child elements of this node (default: document object)
 * @param 	{String}		tag					Optional: Restrict the elements returned to only tags of this type (default: *)
 * 
 * @return	{Array}			An array of elements which match the search parameters, or an empty array if no matching element found
*/
Phoenix.DOM.getElementsByClassName = function (searchclass, node, tag)
{
	var classelements = new Array();
	
	node = node || document;
	tag = tag || '*';

	var els = node.getElementsByTagName(tag);
	var elsLen = els.length;
	var pattern = new RegExp("(^|\\s)"+searchclass+"(\\s|$)");

	for(i=0, j=0; i<elsLen; i++)
	{
		if(pattern.test(els[i].className))
		{
			classelements[j] = els[i];
			j++;
		}
	}
	
//	if(classelements.length <= 0)
//		return(null);

	return(classelements);
};

// Optional: do we want to use Phoenix.DOM.getElementsByClassName() as a library, or do we want to extend the document object to support it?
if(!document.getElementsByClassName)
{
	document.getElementsByClassName = Phoenix.DOM.getElementsByClassName;
}



/**
 * Add the passed CSS class name to the passed element.
 * 
 * @param {object} element A reference to the HTML element to apply the class to
 * @param {String} cssclass The CSS class to apply to the element
 * 
 * @return True if we successfully add the class (or it is already applied to the element), false if the element does not exist, or does not have a className property associated with it.
*/
Phoenix.DOM.addCSSClass = function(element, cssclass)
{
	if(!element || element.className == undefined)
		return(false);
		
	var elclass = element.className;
	
	var re = new RegExp("(^|\\s+)"+cssclass+"(\\s+|$)");
	if(elclass.match(re))
		return(true);
		
	element.className = (elclass == "")? cssclass: elclass+" "+cssclass;
	
	return(true);
}



/**
 * Remove the passed CSS class name from the passed element.
 * 
 * @param {object} element A reference to the HTML element to remove the class from
 * @param {String} cssclass The CSS class to remove from the element
 * 
 * @return True if class successfully removed or no class found, false if passed element is invalid or does not have a className property 
*/
Phoenix.DOM.removeCSSClass = function(element, cssclass)
{
	if(!element || element.className == undefined)
		return(false);
		
	var elclass = element.className;
	
	var re = new RegExp("(^|(?:.*\\s+))"+cssclass+"((?:\\s+.*)|$)");
	while(matches = elclass.match(re))
		elclass = matches[1] + " " + matches[2];
		
	element.className = elclass;

	return(true);
}


/**
 * Get the position of an element's top-left corner in absolute (page/document-relative) co-ordinates - KNOWN BUGS.
 * 
 *  <br /><br />KNOWN BUGS IN SOME BROWSERS - due to the extremely inconsistent and buggy definitions of offsetLeft, offsetTop and offsetParent in various browsers, this function cannot be trusted to work in all situations.  It may be used, but watch out for new bugs thrown up by new browser/element/parent element/CSS combinations, and be prepared to add new special cases to the function if so.
 * 
 * @param		{object}		element	Element whose co-ordinates we're retrieving.
 * @param		{boolean}		_isrecursing Boolean used internally to detect when the function has called itself recursively.  Always ignore or pass a value of false.
 * 
 * @return	{object}		A hash table with two keys (x and y), representing the co-ordinates of the element's top-left-most border pixel, relative to the top-left-most pixel of the document
*/
Phoenix.DOM.getAbsolutePosition = function (element, _isrecursing)
{
	_isrecursing = _isrecursing || false;
	var useragent = window.navigator.userAgent;

	var isSafari = /\bwebkit\b/i.test(useragent);
	var isSafari2 = isSafari && parseInt(version) < 522;

	var isOpera = (/\bopera\b/i.test(useragent)) && (window.opera ? true : false);

	var isIE = /\bMSIE\b/i.test(useragent) && !isOpera;
	var triggersOffsetLeftBugInIE = (isIE && element.style.position == "relative") ? true: false;	// Triggers a bug in IE where child elements' offsetTop values are resolved relative to this element (as they should be), but it offsetLeft values aren't (they remain relative to the next offsetParent up the tree which has position:absolute set).  I'm not joking.

	var isMozilla = (/\bmozilla\b/i.test(useragent)) && !(/\b(compatible|webkit)\b/i.test(useragent));

  var posX = 0;
  var posY = 0;
  
  if(element == null || element.tagName.toLowerCase() == "body" || element.tagName.toLowerCase() == "html")
	  return {x:0, y:0};
	else
		var parentoffsets = this.getAbsolutePosition(element.offsetParent, true);	// Recursively call so we start at the root element and work our way back to the desired one
	
	posX = parentoffsets.x;
	posY = parentoffsets.y;
	
	// Opera includes border-widths in offsetLeft calculations (so it needs no adjustment), and IE doesn't reliably expose computed styles (so we have to use another method)
	if((isSafari && !isSafari2) || isMozilla && !/^t(able|d|h)$/i.test(element.tagName))	// Safari < 2 doesn't include border-widths and Mozilla includes border-widths for *some* elements.
	{
		if(_isrecursing)	// Finally, if we've worked our way right back to the selected element, don't include this element's 	border width because we want the co-ords of the top/left-most pixel of the border
		{
			posX += parseInt(Phoenix.DOM.getComputedStyle("border-left-width", element));
			posY += parseInt(Phoenix.DOM.getComputedStyle("border-top-width", element));
		}
	}
	if(isIE)
	{
		if(element.tagName != "HTML")
		{
			if(_isrecursing)
			{
				if(!triggersOffsetLeftBugInIE)
					posX += element.clientLeft;
				posY += element.clientTop;
			}
		}
	}

	if(!triggersOffsetLeftBugInIE)
		posX += element.offsetLeft;
	posY += element.offsetTop;
	

	return {x:posX,y:posY};
};



/**
 * Retrieve the coordinates of an element relative to the top-left corner of another element - KNOWN BUGS.  The first element does not have to be within - or a child of - the second element.
 * 
 * <br /><br />KNOWN BUGS IN SOME BROWSERS - due to its reliance on {@link #getAbsolutePosition}, this function should also be used cautiously.  See {@link #getAbsolutePosition} for more information.
 * 
 * @param {object} element HTML element to retrieve the co-ordinates of
 * @param {String} srcelement HTML element whose top-left-most border pixel will serve as the origin for the co-ordinate system
 * 
 * @return Hash table with two keys (x and y), representing the co-ordinates of the element's top-left-most border pixel relative to the top-left-mostp ixel of the document
*/
Phoenix.DOM.getRelativePosition = function (element, srcelement)
{
	event = event || window.event;	// Allow for IE-type deviations
	
	var srccoords = this.getAbsolutePosition(srcelement);
	var destcoords = this.getAbsolutePosition(element);
	
	var posx, posy;

	posx = destcoords.x - srccoords.x;
	posy = destcoords.y - srccoords.y;
	
	return({x:posx, y:posy});
};




/**
 * Retrieve the computed CSS style of an element.
 * 
 * @param {String} property A CSS property name (in the form "border-left-width" - not camelcased like "borderLeftWidth")
 * @param {object} element The HTML element to retrieve the computed attribute for
 * @param {String} pseudoelement An (optional) pseudoelement (eg ":first-line")
 *  
 * @return The computed value of the specified element/pseudoelement's specified property
*/
Phoenix.DOM.getComputedStyle = function (property, element, pseudoelement)
{
	if(element)
	{
		if(document.defaultView && document.defaultView.getComputedStyle)
		{
			var styleobj = document.defaultView.getComputedStyle(element, pseudoelement);
			return(styleobj.getPropertyValue(property));
		}
		else if(element.currentStyle)
		{
			// IE doesn't support getting properties by their CSS property name, oh no.  IE - uniquely - demands javascript-style camelCased property names.  Sigh.
			var propertyparts = property.split("-");
			
			var camelCasedProperty = propertyparts[0];
			for(var i=1; i<propertyparts.length; i++)
			{
				var firstchar = propertyparts[i].substring(0,1);
				firstchar = firstchar.toUpperCase();
				camelCasedProperty += firstchar + propertyparts[i].substr(1);
			}
			return(element.currentStyle.getAttribute(camelCasedProperty));
		}
	}
};


/**
 * Generate a new element id that's guaranteed unique in the document.
 * 
 * @param		{String}		prefix		An optional prefix for the name (can be used to make the random ids more recognisable)
 * @param		{int}				minchars	Minimum number of characters to generate (default: 5)
 * @param		{int}				maxchars	Maximum number of characters to generate (default: 15) 
 * 
 * @return	{string}		A generated string beginning with prefix and containing minchars-to-maxchars additional random alphabetic characters, guaranteed not to be the ID of any existing element in the document
*/
Phoenix.DOM.generateUniqueId = function (prefix, minchars, maxchars)
{
	if(minchars > maxchars)
	{
		temp = maxchars;
		maxchars = minchars;
		minchars = temp;
	}
	
	minchars = minchars || 5;
	maxchars = maxchars || 15;
	do
	{
		var randomname = prefix;
		var numletters = (Math.random() * (maxchars - minchars))+minchars;	// Generates names between mindigits and maxdigits letters long
		for(var i=0; i<numletters; i++)
			randomname += String.fromCharCode(97 + Math.round(Math.random() * 25));
	}
	while(document.getElementById(randomname) != null);	// Check in case this name exists (pifflingly small chance, but still...) and if so generate a new one from scratch
		
	return(randomname);
};


/**
 * Get an element's opacity.  Opacity is expressed as a float, where 0.0 is completely transparent and 1.0 is completely opaque.
 * 
 * @param		{object}		element	Element whose opacity we're retrieving
 * 
 * @return	{float}		Floating-point opacity value (0.0-1.0)
*/
Phoenix.DOM.getElementOpacity = function (element)
{
	if(element && element.style)
	{
		if(typeof(element.style.filter) != "undefined")	// Have to try IE first, since although it doesn't understand opacity: in CSS files, it will create a "style.opacity" property, initialise it to the value in the CSS and then flatly ignore it.  Argh!
		{
			var regexp = /alpha\(\s*opacity\s*=\s*(\d+)\s*\)/i;	// Pull out the value from "alpha(opacity=value)" syntax
			var matches = element.style.filter.match(regexp);
			if(matches)
				return(parseFloat(Math.round(matches[1])/100, 10));														// And return it as a 0-1 float (so it's comparable to style.opacity values from *proper* browsers)
		}

		else if(typeof(element.style.opacity) != "undefined")	// Try W3C method
			return(parseFloat(element.style.opacity, 10));

	}
	
	return(null);
}


/**
 * Set an element's opacity.  Opacity is expressed as a float, where 0.0 is completely transparent and 1.0 is completely opaque.
 * 
 * @param		{object}		element	Element whose opacity we're retrieving
 * @param		{float}			value		Floating-point opacity value (0.0-1.0)
 * 
 * @return	{boolean}		True if element and element.style are non-null, otherwise false.
*/
Phoenix.DOM.setElementOpacity = function (element, value)
{
	if(value > 1)
		value = 1;
	if(value < 0)
		value = 0;
		
	if(element && element.style)
	{
		if(typeof(element.style.opacity) != "undefined")	// Try W3C method
			element.style.opacity = value;

		if(typeof(element.style.filter) != "undefined")		// Fall through to IE method
			Phoenix.DOM.addFilter(element, " progid:DXImageTransform.Microsoft.Alpha(opacity="+Math.round(value*100)+")");

		return(true);
	}
	
	return(false);
}


/**
 * IE-specific: add a filter to the specified element.
 * 
 * @param		{object}		element	Element to whom we're adding a filter
 * @param		{float}			filtertext	Text of the filter to add to the selected element
 * 
 * @return	{boolean}		True if element and element.style are non-null, otherwise false.
*/
Phoenix.DOM.addFilter = function(element, filtertext)
{
	if(!element || !element.style)
		return(false);

	// Match filter names & separate filter name from parameters
	var filterregexp = /DXImageTransform\.Microsoft\.([^\.\(]+).*/i;
	
  if(element.style.filter != "")
	{
		// Split filter string up into an array of separate filter declarations
		var chunksarray = element.style.filter.split(" progid:");
		var filtershash = new Object();
		
		// Iterate over array, packing the filter names into a hash table (to remove duplicates)
		for(var i=0; i<chunksarray.length; i++)
		{
			var matches = chunksarray[i].match(filterregexp);
			if(matches)
			{
				filtershash[matches[1]] = matches[0];
			}
		}

		// Now separate the new filter declaration into name+parameters strings
		var matches = filtertext.match(filterregexp);
		// And pack it into the hash (so if a filter of that type already exists it overwrites it)
		if(matches)
			filtershash[matches[1]] = matches[0];

		// Now read back out of the hash and stick the filters back in a string
		element.style.filter = "";
		for(field in filtershash)
			element.style.filter += " progid:" + filtershash[field];
	}
	else
	{
		element.style.filter = filtertext + " ";
	}
	
	return(true);
	
}

/**
 * IE-specific: remove a filter from the specified element.
 * 
 * @param		{object}		element	Element from whom we're removing a filter
 * @param		{float}			filtername	Name of the filter to remove from the selected element
 * 
 * @return	{boolean}		True if element and element.style are non-null, otherwise false.
*/
Phoenix.DOM.removeFilter = function(element, filtername)
{
	if(!element || !element.style)
		return(false);

	// Match filter names & separate filter name from parameters
	var filterregexp = /DXImageTransform\.Microsoft\.([^\.\(]+).*/i;
	
  if(element.style.filter != "")
	{
		// Split filter string up into an array of separate filter declarations
		var chunksarray = element.style.filter.split(" progid:");
		var filtershash = new Object();
		
		// Iterate over array, packing the filter names into a hash table (to remove duplicates)
		for(var i=0; i<chunksarray.length; i++)
		{
			var matches = chunksarray[i].match(filterregexp);
			if(matches)
			{
				filtershash[matches[1]] = matches[0];
			}
		}

		// Now clear the filter that matches the passed-in filtername
		filtershash[filtername] = null;

		// Now read back out of the hash and stick the filters back in a string
		element.style.filter = "";
		for(field in filtershash)
			if(filtershash[field] != null)
				element.style.filter += " progid:" + filtershash[field];
	}
	else
	{
		element.style.filter = filtertext + " ";
	}
	
	return(true);
};

/**
 * Flash an element's border to aid with debugging
 * 
 * @param		{object}		element	Element whose border we want to flash
 * @param		{int}			time	Number of seconds to falsh for (approximate)
 * 
 * @return	{boolean}		False if function loses context during asynchronous calls to itself, otherwise true
*/Phoenix.DOM.flashElementBorder = function(element, time)
{
	var FlashesPerSecond = 2;
	var oldbordertopcolour = null;
	var oldborderrightcolour = null;
	var oldborderbottomcolour = null;
	var oldborderleftcolour = null;
		
	if(typeof(element) != "object")	// If called asynchronously we won't have any parameters passed-in, so retrieve them from where we earlier stashed them in the global scope
	{
		var savedstate = Phoenix.Util.getLocalState("Phoenix.DOM.flashElementBorder");
		if(!savedstate)
			return(false);
		element = savedstate.element;
		time = savedstate.time;
		oldbordertopcolour = savedstate.oldbordertopcolour;
		oldborderrightcolour = savedstate.oldborderrightcolour;
		oldborderbottomcolour = savedstate.oldborderbottomcolour;
		oldborderleftcolour = savedstate.oldborderleftcolour;
	}
	else	// Called procedurally (to start the animation loop), so turn "time to flash for" parameter into a "number of animation steps" parameter
	{
		time = time || 1;	// Set sensible default of 1 second
		
		time *= FlashesPerSecond*2;	// How many seconds to flash for * number of flashes per second * number of animation steps per flash
		oldbordertopcolour = Phoenix.DOM.getComputedStyle("border-top-color", element).replace(/ /g, "").toLowerCase();
		oldborderrightcolour = Phoenix.DOM.getComputedStyle("border-right-color", element).replace(/ /g, "").toLowerCase();
		oldborderbottomcolour = Phoenix.DOM.getComputedStyle("border-bottom-color", element).replace(/ /g, "").toLowerCase();
		oldborderleftcolour = Phoenix.DOM.getComputedStyle("border-left-color", element).replace(/ /g, "").toLowerCase();
	}

	// Choose highlight colour
	var highlightcolour1 = "#ff0000";
	var highlightcolour2 = "#0000ff";
	
	if(time != 0)	// If still animating
	{
		if(time % 2 == 0)
			element.style.borderColor = highlightcolour1;
		else
			element.style.borderColor = highlightcolour2;
			
		var savedstate = {
			element:element,
			time:time-1,
			oldbordertopcolour:oldbordertopcolour,
			oldborderrightcolour:oldborderrightcolour,
			oldborderbottomcolour:oldborderbottomcolour,
			oldborderleftcolour:oldborderleftcolour
		};
		Phoenix.Util.saveLocalState("Phoenix.DOM.flashElementBorder", savedstate);

		window.setTimeout("Phoenix.DOM.flashElementBorder();", 1000/(FlashesPerSecond*2));
	}
	else					// if animation finished
	{
		element.style.borderTopColor = oldbordertopcolour;
		element.style.borderRightColor = oldborderrightcolour;
		element.style.borderBottomColor = oldborderbottomcolour;
		element.style.borderLeftColor = oldborderleftcolour;
	}
	
	return(true);
};