/**
 * Miscellaneous javascript utility functions.
 * 
 * Some of these require jQuery.
 * 
 * (c) 2008-2009 Arkena A/S
 * 
 * @author daniel@arkena.dk
 * 
 * Version 1.9.1110
 * 
 * History:
 * 	1.9.1110:	set_request_get_vars No longer URI decodes existing parameters found in the
 * 				base request and then re-encodes them, as this failed with some characters on
 * 				some browsers when iframed. Now, the existing parameters are left as-is
 * 				(assumed already correctly encoded), and only extra parameters added via vars
 * 				are encoded.
 * 	1.9.1029:	Added Date formatting library v1.2.3.
 * 
 */




/**
 * Extending the Array prototype with some neat extras :)
 */
Array.prototype.contains = function(value)
{
	for (i=0, len=this.length; i<len; ++i) {
		if (this[i] === value)
			return true;
	};
	return false;
};
Array.prototype.indexOf = function(value)
{
	for (i=0, len=this.length; i<len; ++i) {
		if (this[i] === value)
			return i;
	};
	return -1;
}



/**
 * Strips the protocol designator and hostname from an url.
 * 
 * Examples:
 * itpc://www.itunes.com/some/feed	=>	/some/feed
 * www.itunes.com/some/feed			=>	www.itunes.com/some/feed
 * 
 * @param	{string}	url													The URL to strip.
 * @return	{string}														The URL without protocol and host name.
 * 
 * Version 1.9.526
 */
function strip_host(url)
{
	return url.replace(/^[^\:]+\:\/\/[^\/]+/i, '');
}




/**
 * Creates a breadcrumb style path to the specified element.
 * 
 * E.g.:
 * path_to('#my_div_id')	=> "html > body > div#outerwrap > div#my_div_id.my_class" 
 * 
 * @param	{mixed}		element												jQuery resolvable element signifier (i.e. a DOM element, jQuery element, or jQuery formatted selection string).
 * @return	{string}														Path to the element.
 * 
 * Version 1.9.526
 */
function path_to(element)
{
	var breadcrumb = '', elements = $.merge($.makeArray($(element)), $(element).parents());
	
	$(elements).each(function(element) {
		breadcrumb = ' > ' + (this.tagName ? this.tagName.toLowerCase() : '') + (this.id ? '#' + this.id : '') + (this.className ? '.' + this.className.split(' ').join('.') : '') + breadcrumb;
	});
	
	return breadcrumb.substr(3);
}




/**
 * Generates a (semi-)random integer-only string.
 * 
 * @return	{string}														A pseudo-random integer-only string.
 * 
 * Version 1.9.526
 */
function get_random_id()
{
	return ('' + (new Date()).getTime() + Math.random()).replace(/[^0-9]/g, '');
}




/**
 * Replaces, adds, and removes GET variables in the supplied request string.
 * 
 * If a blank request string is provided, you'll just get a string with vars
 * converted to get variables, URL style, prepended by '?'.
 * 
 * If an empty vars parameter is provided, you'll just get the request string.
 * 
 * If a vars member equals null, it'll be removed from the request string if
 * it already exists there.
 * 
 * @param	{string}	request												The request string to replace/add/remove vars in.
 * @param	{mixed}		vars										(OPT)	An array or object of vars to replace/add/remove.
 * @return	{string}														The new request string.
 * 
 * Version 1.9.1110
 * 
 * History:
 * 	1.9.1110:	No longer URI decodes existing parameters found in the base request and
 * 				then re-encodes them, as this failed with some characters on some browsers
 * 				when iframed. Now, the existing parameters are left as-is (assumed already
 * 				correctly encoded), and only extra parameters added via vars are encoded.
 *	1.9.526:	First one.
 */
function set_request_get_vars(request, vars)
{
	if (is_empty(vars))
		return request;
	
	// Okay... so URI encoding can get a bit tricksy... Basically, let's try not to be too clever about what's already
	// been written in the request. IE encodes different from FF which encodes different from Konqueror which encodes
	// different from your grammy's undies. So whichever way the incoming request string has been encoded probably
	// matches how the browser wants the stuff encoded, so leave that be, and only encode the vars array, which consists
	// of Javascript values. So, start out by encoding the vars array values, then add stuff from the request directly:
	for (property in vars)
		vars[property] = encodeURIComponent(vars[property]);
	
	// Collect any existing var/value pairs from the provided request string:
	var bit, bits, pair;
	bits = request.split(/\?|&/);
	
	if (!is_empty(request))
	{
		// The first bit in our split array from the request would be that base request, e.g. a protocol and hostname and/or a page:
		request = bits.shift();
		
		// Take each existing var in the request string and add it to the passed vars object, if not already specified there:
		while (bits.length > 0)
		{
			bit = bits.shift();
			pair = bit.split(/=/);
			if (!is_defined(vars[pair[0]]))
				vars[pair[0]] = pair[1];
		}
	}
	
	// Okay, so let's create a string of get vars to append to the request string. We discard null ones:
	var get_vars = '';
	for (property in vars)
		if (vars[property] != null)
			get_vars += property+'='+vars[property]+'&';
	
	// Append the vars to the request string, prepended by a question mark and stripped of the trailing ampersand, if not empty:
	if (get_vars.length > 0)
		request += '?' + get_vars.substring(0, get_vars.length-1);
	
	return request;
}




/**
 * Extracts GET variables from the supplied request string and returns them as an object with matching members.
 * 
 * 
 * @param	{string}	request												The request string to replace/add/remove vars in.
 * @return	{object}														The variables, url decoded.
 * 
 * Version 1.9.1007
 */
function get_request_get_vars(request)
{
	var vars = {};
	
	if (!is_empty(request))
	{
		// Collect any existing var/value pairs from the provided request string:
		var bits = request.split(/\?|&/);
		
		// Throw away the path part:
		bits.shift();
		
		// Take each existing var in the request string and add it as a member to the vars object:
		while (bits.length > 0)
		{
			var pair = bits.shift().split(/=/);
			vars[pair[0]] = decodeURIComponent(pair[1]);
		}
	}
	
	return vars;
}




/**
 * Strips anything outside <script> tags, including the tags themselves,
 * from input html and returns just the script code within these tags.
 * 
 * WARNING: CURRENTLY THIS STRIPS NEWLINES, MAKING FOR UNPREDICTED BEHAVIOR
 * (E.G. YOU MIGHT USE // TO COMMENT SOMEWHERE AND YOU END UP UNCOMMENTING
 * THE REMAINDER OF THE CODE! D'OH! CURRENTLY NOT USED, FOR THE SAME REASON)
 * 
 * @param	{string}	html												The text containing script tags.
 * @return	{string}														Any raw javascript code found inside <script> tags in the html.
 * 
 * Version 1.9.526
 */
function extract_js(html)
{
	// Strip whitespace:
	html = html.replace(/[ \n\f\r\v\t]+/g, ' ');
	
	// Collect script sections:
	var matches = html.match(/<script\b[^>]*>(.*?)<\/script>/gi);
	
	// Got nothing? Return empty string:
	if (is_empty(matches)) return '';
	
	// Strip script tags from matches:
	var code = '';
	for (var i=0; i<matches.length; ++i)
		code += matches[i].replace(/(<script\b[^>]*>[ ]*(<\!--)*)|((\/\/[ ]*--\>)<\/script>)/gi, '');
	
	// Return the extracted, raw code:
	return code;
}




/**
 * If hex_value is a string, it is trimmed and any prefixed
 * hash (#) - as often used in CSS/HTML - is replaced with '0x',
 * which is the correct JS notation, and the string is returned.
 * 
 * @param	{string}	hex_value											The hex value string to clean up.
 * @return	{string}														If hex_value was a string, the cleaned up version, otherwise
 * 																			hex_value is returned unaltered.
 * 
 * Version 1.9.526
 */
function correct_hex_prefix(hex_value)
{
	if (typeof hex_value == 'string')
		return $.trim(hex_value).replace(/^#/, '0x');
	else
		return hex_value;
}




/**
 * Some snippet found somewhere I forgot, works with some browsers on some OSes, but the whole copy to clipboard
 * thing is dead at the onset anyway. We shouldn't be using it, period.
 * 
 * @param	{string}	f													The text to copy to the clipboard.
 * 
 * Version 1.9.526
 */
function clipboard(f)
{
	var bP = typeof f == "undefined";
	if (window.clipboardData)
	{
		if (!bP)
		{
			window.clipboardData.setData("Text", f);
			return;
		}
		return window.clipboardData.getData("Text");
	}
	else
	{
		FT = "text/unicode";
		CI = Components.interfaces;
		Cb = CI.nsIClipboard;
		C = Components;
		Cs = CI.nsISupportsString;
		Cm = "@mozilla.org";
		Cmw = Cm + "/widget/clipboard;1";
		try
		{
			netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
			e = C.classes[Cmw].createInstance(Cb);
			var clip = C.classes[Cmw].getService(Cb);
		} catch (e2) { return; }
		try
		{
			b = C.classes[Cm + "/widget/transferable;1"].createInstance(CI.nsITransferable);
		} catch (e3) { return; }
		b.addDataFlavor(FT);
		if (typeof f == "undefined")
		{
			clip.getData(b, clip.kGlobalClipboard);
			var str = {};
			var strLength = {};
			b.getTransferData(FT, str, strLength);
			if (str)
			{
				str = str.value.QueryInterface(Cs);
			}
			if (str)
			{
				f = str.data.substring(0, strLength.value / 2);
			}
			return f;
		}
		o = C.classes[Cm + "/supports-string;1"].createInstance(Cs);
		o.data = f;
		b.setTransferData(FT, o, f.length * 2);
		try
		{
			t = CI.nsIClipboard;
		} catch (e4) { return; }
		e.setData(b, null, t.kGlobalClipboard);
		return;
	}
	return;
}




/**
 * merge_objects merges an arbitrary number of objects and/or arrays into one object.
 * If the same property exists in more than one object/array, the last one is used.
 * 
 * Version 1.9.1005
 * History:
 * 	1.9.1005: Now works recursively, and jQuery based.
 * 
 * @param	{object}	*													Any number of objects to merge, inverted sequence order equals priority.
 * @return	{object}														Object containing the sum members of all merged objects.
 */
function merge_objects()
{
	obj = {};
	
	if (arguments.length > 0)
		for (var i=0; i<arguments.length; ++i)
			jQuery.extend(true, obj, arguments[i]);
	
	return obj;
}




/**
 * Removes all members that contain null from the provided source object or array.
 * The source is recursively processed.
 * 
 * Version 1.9.1006
 * 
 * @param {Object} source
 */
function object_without_null_members(source)
{
	var obj = {};
	
	if (arguments.length > 0 && (is_object(source) || $.isArray(source)))
	{
		for (var property in source)
		{
			var val = source[property];
			if (val != null)
				obj[property] = ((is_object(val) || $.isArray(val)) && !(val instanceof jQuery)) ? object_without_null_members(val) : val;
		}
	}
	
	return obj;
}




/**
 * Based on the annoying supersleight. Fixes PNG transparency.
 * 
 * Calling this function on any browser other than IE <7 will
 * have no effect.
 * 
 * Version 1.9.526
 */
function fix_ie6_pngs(args)
{
	// Only if the user's browser reports itself as Internet Explorer version < 7:
	if (!$.browser.msie || parseInt($.browser.version) >= 7) return; // TODO: As of jQuery 1.3, $.browser is deprecated and will be removed. We should use some other approach, perhaps $.support.opacity (although this seems to target all IEs? (http://docs.jquery.com/Utilities/jQuery.support))
	
	if (is_empty(arguments)) args = {};
	
	// If no particular root defined, we process the entire document:
	var root				= is_defined(args.root) ? document.getElementById(args.root) : document;
	var apply_positioning	= is_defined(args.apply_positioning) ? args.apply_positioning : true;
	
	var shim = 'http://res.arkena.dk/img/shared/1x1_transparent.gif';	// Path to a transparent GIF image.
	
	for (var i=root.all.length-1, obj=null; (obj=root.all[i]); --i)
	{
		// Process background PNGs:
		if (obj.currentStyle.backgroundImage.match(/\.png/i) !== null)
		{
			var mode	= (obj.currentStyle.backgroundRepeat == 'no-repeat') ? 'crop' : 'scale';
			var bg		= obj.currentStyle.backgroundImage;
			var src		= bg.substring(5,bg.length-2);
			
			obj.style.filter			= "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + src + "', sizingMethod='" + mode + "')";
			obj.style.backgroundImage	= 'url('+shim+')';
		}
		// Process image elements:
		if (obj.tagName=='IMG' && obj.src.match(/\.png$/i) !== null)
		{
			var src = obj.src;
			obj.style.width		= obj.width + "px";
			obj.style.height	= obj.height + "px";
			obj.style.filter	= "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + src + "', sizingMethod='scale')";
			obj.src				= shim;
		}
		// Apply position to 'active' elements if requested:
		if (apply_positioning && (obj.tagName=='A' || obj.tagName=='INPUT') && obj.style.position === '')
		{
			obj.style.position = 'relative';
		}
	}
};




/**
 * Checks whether a given element is defined or not.
 * 
 * @param	{mixed}		mixed												The element to check whether ot not is defined.
 * @return	{bool}															True if it's defined, false if not.
 * 
 * Version 1.9.526
 */
function is_defined(mixed)
{
	return typeof mixed != "undefined";
}




/**
 * Checks whether a given element is empty or not.
 * Empty means either undefined, null, or an empty array or string.
 * 
 * @param	{mixed}		mixed												The element to check whether or not is empty.
 * @return	{bool}															True if it's empty, false if not.
 * 
 * History:
 * 	1.9.1007: Woops, apparently 0 == '', so changed the comparison operator there to strict.
 * 
 * Version 1.9.1007
 */
function is_empty(mixed)
{
	return !is_defined(mixed) || mixed == null || mixed === '' || mixed.length == 0;
}




/**
 * Checks whether provided argument is an object or not. Arrays and nulls don't count as objects.
 * 
 * @param	{mixed}		mixed												The element to check whether or not is empty.
 * @return	{bool}															True if it's empty, false if not.
 * 
 * History:
 * 	1.9.1006: Arrays and nulls no longer count.
 * 
 * Version 1.9.1006
 */
function is_object(mixed)
{
	return mixed != null && !$.isArray(mixed) && typeof mixed == 'object';
}




/**
 * Takes an arbitrary element and returns it as a jQuery object.
 * 
 * If element is a jQuery object, it is returned unaltered.
 * If element is an object, it is converted to a jQuery object and returned.
 * If element is a string and returns a non-empty jQuery object when prepended with '#' this is returned.
 * If element is a string, it is assumed to be a jQuery formatted selector, converted to a jQuery object and returned.
 * 
 * @param	{mixed}		element												The element to jQuerify.
 * @return	{object}														jQuery object.
 * 
 * Version 1.9.526
 */
function jquerify_element(element)
{
	if (is_empty(arguments) || is_empty(element))
		return $();
	
	// Easy...
	if (element instanceof jQuery)
		return element;
	
	// Tricky. Take myLiteral = 'hello' vs. myString = new String('hello'). myLiteral is not a String, it's
	// not even an Object, it's a primitive. On the other hand, myString is of course both an Object and
	// a String. So, to know we have any type of Object other than String, we must check for both:
	if (element instanceof Object && !(element instanceof String))
		return $(element);
	
	// Now, we just assume string. Let's check if it's the id of an existing dom element:
	if ($('#'+element).length > 0)
		return $('#'+element);
	
	// Otherwise, let's just jQuery it:
	return $(element);
}




/*
 * Date Format 1.2.3
 * (c) 2007-2009 Steven Levithan <stevenlevithan.com>
 * MIT license
 *
 * Includes enhancements by Scott Trenda <scott.trenda.net>
 * and Kris Kowal <cixar.com/~kris.kowal/>
 *
 * Accepts a date, a mask, or a date and a mask.
 * Returns a formatted version of the given date.
 * The date defaults to the current date/time.
 * The mask defaults to dateFormat.masks.default.
 */
var dateFormat = function () {
	var	token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
		timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
		timezoneClip = /[^-+\dA-Z]/g,
		pad = function (val, len) {
			val = String(val);
			len = len || 2;
			while (val.length < len) val = "0" + val;
			return val;
		};

	// Regexes and supporting functions are cached through closure
	return function (date, mask, utc) {
		var dF = dateFormat;

		// You can't provide utc if you skip other args (use the "UTC:" mask prefix)
		if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
			mask = date;
			date = undefined;
		}

		// Passing date through Date applies Date.parse, if necessary
		date = date ? new Date(date) : new Date;
		if (isNaN(date)) throw SyntaxError("invalid date");

		mask = String(dF.masks[mask] || mask || dF.masks["default"]);

		// Allow setting the utc argument via the mask
		if (mask.slice(0, 4) == "UTC:") {
			mask = mask.slice(4);
			utc = true;
		}

		var	_ = utc ? "getUTC" : "get",
			d = date[_ + "Date"](),
			D = date[_ + "Day"](),
			m = date[_ + "Month"](),
			y = date[_ + "FullYear"](),
			H = date[_ + "Hours"](),
			M = date[_ + "Minutes"](),
			s = date[_ + "Seconds"](),
			L = date[_ + "Milliseconds"](),
			o = utc ? 0 : date.getTimezoneOffset(),
			flags = {
				d:    d,
				dd:   pad(d),
				ddd:  dF.i18n.dayNames[D],
				dddd: dF.i18n.dayNames[D + 7],
				m:    m + 1,
				mm:   pad(m + 1),
				mmm:  dF.i18n.monthNames[m],
				mmmm: dF.i18n.monthNames[m + 12],
				yy:   String(y).slice(2),
				yyyy: y,
				h:    H % 12 || 12,
				hh:   pad(H % 12 || 12),
				H:    H,
				HH:   pad(H),
				M:    M,
				MM:   pad(M),
				s:    s,
				ss:   pad(s),
				l:    pad(L, 3),
				L:    pad(L > 99 ? Math.round(L / 10) : L),
				t:    H < 12 ? "a"  : "p",
				tt:   H < 12 ? "am" : "pm",
				T:    H < 12 ? "A"  : "P",
				TT:   H < 12 ? "AM" : "PM",
				Z:    utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
				o:    (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
				S:    ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
			};

		return mask.replace(token, function ($0) {
			return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
		});
	};
}();
// Some common format strings
dateFormat.masks = {
	"default":      "ddd mmm dd yyyy HH:MM:ss",
	shortDate:      "m/d/yy",
	mediumDate:     "mmm d, yyyy",
	longDate:       "mmmm d, yyyy",
	fullDate:       "dddd, mmmm d, yyyy",
	shortTime:      "h:MM TT",
	mediumTime:     "h:MM:ss TT",
	longTime:       "h:MM:ss TT Z",
	isoDate:        "yyyy-mm-dd",
	isoTime:        "HH:MM:ss",
	isoDateTime:    "yyyy-mm-dd'T'HH:MM:ss",
	isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
};
// Internationalization strings
dateFormat.i18n = {
	dayNames: [
		"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
		"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
	],
	monthNames: [
		"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
		"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
	]
};
// For convenience...
Date.prototype.format = function (mask, utc) {
	return dateFormat(this, mask, utc);
};
