/**
 * Abilitation JavaScript Library
 * 
 * @author Neil Martin
 * @version 1.0
 */

// JSLint
// var $, jQuery, window, document, escape;


// Define standard JavaScript library namespaces.
// Note, additional namespaces may be attached the the root Abl object
var Abl = {};
Abl.Cms ={};
Abl.Cookie = {};
Abl.DateTime = {};
Abl.DEBUG = {};
Abl.IO = {};
Abl.Json = {};
Abl.Math = {};
Abl.String = {};
Abl.UI = {};
Abl.Window = {};

Abl.version = "1.0.0";

/******************************************************************
** Abl - Core Functions
******************************************************************/
/**
 * chain()
 *
 * Chains an array of functions together and calls via the context
 * provided
 */
Abl.chain = function(oldFunc, newFunc) {
	var funcList, i;
	if (typeof oldFunc !== 'function') {
		return newFunc;
	} else {
		funcList = [oldFunc, newFunc];
		return function() {  
			for (i = 0; i < funcList.length; i++) {  
				funcList[i]();  
			}
		};
	}
};


/******************************************************************
** Abl.DateTime - Core Functions
******************************************************************/
/**
 * Converts a number representing a time period to milliseconds
 * 
 * @param {Number} n The time to be converted
 * @param {String} t Where 's'==seconds, 'm'==minutes, 'h'==hours, 'd'==days, 'w'==weeks
 */
Abl.DateTime.toMilliseconds = function(n, t) {
	switch (t) {
		case "s": return (n * 1000);
		case "m": return (n * 60 * 1000);
		case "h": return (n * 60 * 60 * 1000);
		case "d": return (n * 24 * 60 * 60 * 1000);
		case "w": return (n * 7 * 24 * 60 * 60 * 1000);
		default: return n;
	}
};


/******************************************************************
** Abl.Cookie - Core Functions
******************************************************************/
/**
 * Stores a named cookie
 * 
 * @param {String} name			The name of the cookie
 * @param {String} value		The cookie's value
 * @param {Number} ms			The cookie's expiration period
 * @param {String} timePeriod 	See Abl.DateTime.toMilliseconds()
 */
Abl.Cookie.set = function(name, value, t, timePeriod) {
	var date, expires;
	if (t) {
	 	date = new Date();
	 	date.setTime(date.getTime() + Abl.DateTime.toMilliseconds(t, timePeriod));
	 	expires = "; expires=" + date.toGMTString();
	}
	document.cookie = name + "=" + escape(value) + expires + "; path=/";
};

/**
 * Rerieves a named cookie
 * 
 * @param {String} name			The name of the cookie to be retrieved
 */
Abl.Cookie.get = function(name) {
	var	nameEQ = name + "=",
			ca = document.cookie.split(';'),
			i, c;

	for (i = 0; i < ca.length; i++) {
		c = ca[i];
		while (c.charAt(0) === ' ') {
		  	c = c.substring(1, c.length);
		}
		if (c.indexOf(nameEQ) === 0) {
			return c.substring(nameEQ.length, c.length);
		}
	}
	return null;
};

/**
 * Retrieves a named cookie as an integer value
 * 
 * @param {String} name			The name of the cookie to be retrieved
 */
Abl.Cookie.getInt = function(name) {
	var x = Abl.Cookie.get(name);
	return (x) ? parseInt(x, 10) : 0;
};

/**
 * Retrieves a named cookie as an float value
 * 
 * @param {String} name			The name of the cookie to be retrieved
 */
Abl.Cookie.getFloat = function(name) {
	var x = Abl.Cookie.get(name);
	return (x) ? parseFloat(x) : 0.0;
};


/******************************************************************
** Abl.Math - Core Functions
******************************************************************/
/**
 * Forces the supplied value to fall within the specified bounds.
 * 
 * @param {Number} val	The value to be constrained
 * @param {Number} min	The lowest allowable value
 * @param {Number} max	The highest allowable value
 */
Math.constrain = function(val, min, max){
	return Math.min(Math.max(val, min), max);
};

Math.getInt = function(o, radix) {
	var x = parseInt(o, radix || 10);
	return (isNaN(x) ? 0 : x);
};

/**
 * Forces the width/height parameters to fall within the specified range.
 *
 * @param {Integer} width			Original width
 * @param {Integer} height			Original height
 * @param {Integer} maxWidth		Maximum allowable width
 * @param {Integer} maxHeight		Maximum allowable height
 * @return {Array}					An array of the adjusted width/height
 */
Math.forceFit = function(width, height, maxWidth, maxHeight) {
	if (width > maxWidth) {
		height = parseInt(height * (maxWidth / width), 10);
		width = maxWidth;
	}
	if (height > maxHeight) {
		width = parseInt(width * (maxHeight / height), 10);
		height = maxHeight;
	}
	return [width, height];
};



/******************************************************************
** Abl.String - Core Functions
******************************************************************/
/**
 * Strips leading and trailing white-space from the string.
 */
String.prototype.trim = function() {
	return this.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
};

/**
 * Pads the left-hand side of the string to obtain the target
 * length.
 * 
 * @param {String} ch	The character to use as padding
 * @param {Object} len	The target length of the string
 */
String.prototype.padLeft = function(ch, len) {
	var s = this;
	while (s.length < len) {
		s = ch + s;
   }
	return s;
};

/**
 * Pads the right-hand side of the string to obtain the target
 * length.
 * 
 * @param {String} ch	The character to use as padding
 * @param {Number} len	The target length of the string
 */
String.prototype.padRight = function(ch, len) {
	var s = this;
	while (s.length < len) {
		s += ch;
   }
	return s;
};


/**
 * Truncates a string of text back to the nearest word-break falling
 * on or before the maximum allowable length.  Optionally
 * adds a suffix string to the truncated string.
 *
 * @param {Number}	maxLength	The maximum allowable string length
 * @param {Number}	range		The number of characters to work back
 * 								from the specified break point in order
 * 								to find a word-break
 * @param {String}	suffix		String to be appended to the truncated 
 * 								result
 */
String.prototype.trimToIntro = function(maxLength, range, suffix) {
	var text = this, i;
	if (text.length > maxLength)
	{
		i = maxLength - range;
		if (i < 0) { i = 0; }
		while (((i < maxLength) && (text.substr(i, 1) !== " ")))
		{
			i++;
		}
		text = text.substr(0, i).trim() + suffix;
	}
	return text;
};


/******************************************************************
** Abl.UI - Core Functions
******************************************************************/
/**
 * Returns the height of the document in pixels.
 * @return {Number} height of the document
 */
Abl.UI.getPageHeight = function() {
	return document.body.scrollHeight;
};

/**
 * Returns the width of the document in pixels.
 * @return {Number} width of the document
 */
Abl.UI.getPageWidth = function() {
	return document.body.scrollWidth;
};

/**
 * Returns the height of the browser viewport in pixels.
 * @return {Number} height of the viewport
 */
Abl.UI.getWindowHeight = function() {
	return $(window).height();
};

/**
 * Returns the width of the browser viewport in pixels.
 * @return {Number} width of the viewport
 */
Abl.UI.getWindowWidth = function() {
	return $(window).width();
};



/******************************************************************
** Abl.Window - Core Functions
******************************************************************/
/**
 * Creates a new Uri() object representing a web address.
 * 
 * The constructor will optionally take a string or a html
 * anchor tag element to specify the address,  If no parameter
 * is specified the current page's address is used (via the
 * window.location.href property)
 * 
 * @constructor
 * @param {String, Object} [The web address reference]
 * @return {Object}
 */
Abl.Window.Uri = function(ref) {
	this.href = '';
	if (typeof ref === 'string') {
		this.href = ref;
	} else 
	if ((typeof ref === 'object')&&(ref.href)) {
		this.href = ref.href;
	} else
	if ((ref)&&(ref instanceof Abl.Window.Uri)) {
		this.href = ref.href;		
	} else
	if ((ref)&&(ref instanceof jQuery)&&(ref[0])&&(ref[0].href)) {
		this.href = ref[0].href;		
	} else {
		this.href = window.location.href;
	}
};

/**
 * Returns the query string element of the Uri object.
 * 
 * @return {string} Returns the Uri's query string
 */
Abl.Window.Uri.prototype.getQueryString = function() {
	var i = this.href.indexOf("?");
	if (i < 0) {
		return '';
	} else {
		return this.href.substr(i+1);
	}
};

/**
 * Returns the local address of the Uri object. For example:
 * 
 * 	http://www.myweb.com/ContactUs/index.html?type=enquiry
 * 	/ContactUs/index.html?type=enquiry
 * 
 * @return {String} The local address
 */
Abl.Window.Uri.prototype.getLocalUrl = function() {
	return Abl.Window.Uri.stripLocation(this.href);
};

/**
 * Returns the base/core url of the Uri object. For example:
 * 
 * 	http://www.myweb.com/ContactUs/index.html?type=enquiry
 * 	/ContactUs/index.html
 * 
 * @return {String} The base address
 */
Abl.Window.Uri.prototype.getBaseUrl = function() {
	var s = Abl.Window.Uri.stripLocation(this.href);
	return Abl.Window.Uri.stripQueryString(s);
};


/**
 * Strips the current location's protocol information from
 * the supplied href/url.
 * 
 * @param {Object}	Reference to the href string/Uri() constructor object
 * @return {String}	The original href less the protocol
 */
Abl.Window.Uri.stripProtocol = function(ref) {
	var uri = new Abl.Window.Uri(ref);
	if (uri.href.indexOf(window.location.protocol) === 0) {
		return uri.href.substr(window.location.protocol.length);
	} else {
		return uri.href;
	}
};


/**
 * Strips the current location's protocol, hostname and port 
 * information from the supplied href/url.
 * 
 * @param {Object}	Reference to the href string/Uri() constructor object
 * @return {String}	The original href less the protocol, hostname
 * 					and port information
 */
Abl.Window.Uri.stripLocation = function(ref) {
	var	uri = new Abl.Window.Uri(ref),
			domain = window.location.protocol + '//' + window.location.hostname;

	if (window.location.port) {
		domain += ":" + window.location.port;
	}
	if (uri.href.indexOf(domain) === 0) {
		return uri.href.substr(domain.length);
	} else {
		return uri.href;
	}
};

/**
 * Strips the current location's query string data
 * from the supplied href/url.
 * 
 * @param {Object}	Reference to the href string/Uri() constructor object
 * @return {String}	The original href less the query string/bookmark
 */
Abl.Window.Uri.stripQueryString = function(ref) {
	var uri = new Abl.Window.Uri(ref), i;
	
	// Strip everyting after the first '?' or '#' ...
	i = uri.href.search(/(\?|#)/);	
	if (i >= 0) {
		uri.href = uri.href.substr(0,i);
	}
	return uri.href;
};

/**
 * Strips the current location's protocol, hostname, port 
 * and query string information from the supplied href/url.
 * 
 * @param {Object}	Reference to the href string/Uri() constructor object
 * @return {String}	The original href less the protocol,
 * 					hostname, port and query string information
 */
Abl.Window.Uri.getBaseUrl = function(ref) {
	var	uri = new Abl.Window.Uri(ref),
			s = Abl.Window.Uri.stripLocation(uri.href);
	return Abl.Window.Uri.stripQueryString(s);
};


Abl.Window.Uri.parseQueryString = function(queryString) {

  // define an object to contain the parsed query data
  var	result = {},
		queryComponents,
		i, keyValuePair, key, value;

  // if a query string wasn't specified, use the query string from the URI
  if (typeof queryString === "undefined") {
    queryString = window.location.search ? window.location.search : '';
  }

  // remove the leading question mark from the query string if it is present
  if (queryString.charAt(0) === "?") {
  	queryString = queryString.substring(1);
  }

  // replace plus signs in the query string with spaces
  queryString = queryString.replace('+', ' ');

	// split the query string around ampersands and semicolons
	queryComponents = queryString.split(/[&;]/g);

	// loop over the query string components
	for (i = 0; i < queryComponents.length; i++){
		// extract this component's key-value pair
		keyValuePair = queryComponents[i].split('=');
		key = decodeURIComponent(keyValuePair[0]);
		value = decodeURIComponent(keyValuePair[1]);
		result[key] = value;
	}

	// return the parsed query data
	return result;
};


Abl.Window.Uri.getParam = function(n, queryString) {
	var qs = Abl.Window.Uri.parseQueryString(queryString);
	return (qs) ? qs[n] : null;
};




/****************************************************************************/
/** Extensions to base jQuery functionality                                **/
/****************************************************************************/
(function($) {

	$.fn.setFocus = function(selectContent) {
		return this.each(function() {
			if (($(this).is("input")) && (!this.disabled)) {
				if (this.focus) { this.focus(); }
				if ((selectContent) && (this.select)) { this.select(); }
			}
		});
	};


	/**
	 * $.wrapper()
	 * 
	 * Wraps the inner content of an element with a <div /> for each class of 
	 * the classList array.  This method is used, for example, by the panel
	 * method to add additional markup for css styling.
	 * 
	 * @param {Array} classList
	 */
	$.fn.wrapper = function(classList) {
		classList = classList || $.fn.wrapper.defaultList;
		return this.each(function() {
			var $this = $(this), i, wrapper;
			for (i = classList.length - 1; i >= 0; i--) {
				wrapper = "<div class='" + classList[i] + "'></div>";
				$this.wrapInner(wrapper);
			}
		});
	};
	
	/**
	 * The default wrapper classList for adding the classic '8-points of the compass'
	 * markup.
	 */
	$.fn.wrapper.defaultList = ["e", "s", "w", "ne", "se", "sw", "nw", "panelBody"];
	


	/**
	 * $.panel()
	 * 
	 * Wraps the inner content of the specified elements with a nested structure
	 * of div elements which can then have 8-points of images applies to them.
	 * 
	 * @param {String} className	The css class name of the panel theme
	 * @param {Object} options		Property list of options.
	 * 
	 * @see jquery.ui.draggable
	 */
	$.fn.panel = function(className, options) {
		var params = $.extend({}, $.fn.panel.defaults, options);
		
		return this.each(function(){
			var	$e = $(this),
					$panelBody,
					$titlebar,
					$header,
					$closeLink;
			
			if (className) { $e.addClass(className); }
			$e.css(params.css);
			
			// Add additional markup for '8-points of th compass' styling - note
			// use of default $.fn.wrapper.default array.
			$e.wrapper();

			// Get a reference to the inner content			
			$panelBody = $("div.panelBody", $e);


			// Set dimensions
			if (params.width) {
				$e.width(params.width);
			}
			if (params.height) {
				$panelBody.height(params.height);
			}
			
			if ((params.innerWidth) || (params.innerHeight)) {
				$e.css("position", "absolute");
				$panelBody.width(params.innerWidth);
				$panelBody.height(params.innerHeight);
			}
			
			// Manage the creation and presentation of the titlebar if specified
			if (params.titlebar) {
				// Create a titlebar and insert it before the existing
				// (inner) content
				$titlebar = $("<div class='titlebar'></div>");
				$panelBody.before($titlebar);
				
				// If title text has been specified, use it!
				if (params.title) {
					$titlebar.append("<h2 class='title'>" + params.title + "</h2>");
				} else {
					// Otherwise, see if the first element of the (inner) content is
					// a header (h1, h2, h3 etc.).  If it is, move it into the
					// titlebar container - neat eh? :-)
					$header = $panelBody.children(":first-child:header").addClass("title");
					if ($header.length) {
						$titlebar.append($header);
					}
				}
			}

			if (params.closeLink) {
				$closeLink = $("<a></a>")
				.addClass(params.closeLinkClass)
				.attr("title", params.closeLinkTitle)
				.attr("href", params.closeLinkHref)
				.text(params.closeLinkText)
				.appendTo($titlebar);
			}

			// Configure jquery.ui.draggable features
			if (params.draggable) {
				$e.draggable($.extend({handle: ($titlebar) ? $titlebar : $e}, params.dragOptions));
			}
		});
	};

	/**
	 * Default option settings for $panel
	 * Note, additional jquery.ui.draggable options may be added to this
	 * property list.
	 */
	$.fn.panel.defaults = {
		width:				null,
		height:				null,
		innerWidth:			null,
		innerHeight:		null,
		titlebar:			true,			// Set to 'true' to render a title bar container
		title:				"",			// Titlebar text
		closeLink:			false,
		closeLinkClass:	"close",
		closeLinkTitle:	"Close",
		closeLinkHref:		"#close",
		closeLinkText:		"X",
		css: { },
		draggable:			false,		// Set to 'true' to enable dragging
		dragOptions: {						// jquery.ui.draggable options list
			containment:	"parent"		// jquery.ui.draggable option
		}
	};

	/**
	 * Calculates the overall distances (width/height) between the
	 * outer container ($panel) and the content (div.panelBody).
	 * 
	 * @param	{Object} $panel	The jQuery panel to be interrogated
	 * @return	{Object}		A width, height dictionary object
	 */
	$.fn.panel.getPanelOffsets = function($panel) {
		var	width = $panel.width(),
				height = $panel.height(),
				$panelBody = $("div.panelBody", $panel),
				innerWidth = $panelBody.width(),
				innerHeight = $panelBody.height();

		return {
			width: width - innerWidth,
			height: height - innerHeight	
		};	
	};


	$.fn.panel.setContentArea = function($panel, width, height) {
		var	offset = $.fn.panel.getPanelOffsets($panel),
				$panelBody = $("div.panelBody", $panel);
		
		if (width) {
			$panel.width(width + offset.width);
			$panelBody.width(width);
		}
		
		if (height) {
			$panel.height(height + offset.height);
			$panelBody.height(height);
		}
	};


	/**
	 * Provide IE6 support for min-height
	 * @param {Integer} minHeight
	 */
	$.fn.minHeight = function(minHeight) {
		return this.each(function() {
			var $elem = jQuery(this);
			if ((!minHeight)||(typeof(minHeight) !== "number")) {
				minHeight = parseInt($elem.css("min-height"), 10);
			}
			if ((minHeight)&&(typeof(minHeight) === "number") && ($elem.height() < minHeight)) {
				$elem.height(minHeight);
			}
		});
	};



	/**
	 * Provide IE6 support for min-width
	 * @param {Integer} minHeight
	 */
	$.fn.minWidth = function(minWidth) {
		return this.each(function() {
			var $elem = jQuery(this);
			if ((!minWidth)||(typeof(minWidth) !== "number")) {
				minWidth = parseInt($elem.css("min-width"), 10);
			}
			if ((minWidth)&&(typeof(minWidth) === "number") && ($elem.width() < minWidth)) {
				$elem.width(minWidth);
			}
		});
	};

})(jQuery);



/**
 * Objectifies the jQuery.panel() method result to provide an OO panel
 * class.
 * 
 * This class exposes the following jQuery objects:
 * 
 * 	.$container		The outer div.className container
 * 	.$body			The inner div.panelBody content container
 * 	.$titlebar		The div.titlebar container (if specified)
 * 	.closeLink		The a.closeLinkClass close link (if specified)
 * 
 * The class maps the following events:
 * 
 * 	onClose			Raised when the $closeLink link is clicked
 */
Abl.UI.Panel = function(panel, className, options) {
	var	_self = this,
			params = $.extend(true, {}, $.fn.panel.defaults, Abl.UI.Panel.defaults, options);
	
	this.$ = (panel instanceof jQuery) ? panel : $(panel);
	this.$.panel(className, params);
	this.$body = $("div.panelBody", this.$);
	this.$titlebar = $("div.titlebar", this.$);
	this.$closeLink = $("a." + params.closeLinkClass, this.$).click(function(evt) {
		evt.preventDefault();
		if (typeof params.onClose === 'function') {
			params.onClose.call(_self, evt);
		}		
	});
	
	this.setContentArea = function(width, height) {
		$.panel.setContentArea(this.$, width, height);
	};
	
	this.isVisible = function() {
		return this.$.is(":visible");
	};
	
	
	this.dispose = function() {
		this.$closeLink.unbind("click");
	};
};


/**
 * Extends $.fn.panel.defaults
 */
Abl.UI.Panel.defaults = {
	onClose: null
};



/*
** ImagePreLoader
** Loads an array of images of the form...
**
**		var gallery = [
**			{ src: "/img/bathroom.jpg", alt: "Stylish Bathroom" },
**			{ src: "/img/tanker.jpg", alt: "Drainage Tanker" }
**		];
**		var images = new Abl.UI.ImagePreLoader(gallery);
**
*/
Abl.UI.ImagePreLoader = function(images, options) {
	var	_self = this,
			params = $.extend(true, {}, Abl.UI.ImagePreLoader.defaults, options),
			noImages = images.length,
			i = 0,
			noLoaded = 0,
			noErrors = 0,
			noAborted = 0,
			noProcessed = 0,
			bIsLoaded = false,
			index = 0,
			aImages = [];


	/*
	** Event Handlers
	*/
	function onComplete() {
		noProcessed++;
		if (noProcessed === noImages) {
			bIsLoaded = true;
			if (typeof params.onLoaded === 'function') {
				params.onLoaded.call(_self);
			}
		}
	}

	function onLoad() {
		$(this).data("preload").loaded = true;
		noLoaded++;
		onComplete();
	}

	function onError() {
		$(this).data("preload").error = true;
		noErrors++;
		if (typeof params.onError === 'function') {
			params.onError.call($(this));
		}
		onComplete();
	}

	function onAbort() {
		$(this).data("preload").aborted = true;
		noAborted++;
		if (typeof params.onAbort === 'function') {
			params.onAbort.call($(this));
		}
		onComplete();
	}



	/*
	** Private Methods
	*/
	function loadImage(imageData) {
		var $img = $("<img />");
		aImages.push($img);

		$img.bind("load", onLoad);
		$img.bind("error", onError);
		$img.bind("abort", onAbort);
		$img.data("preload", { loaded: false, error: false, aborted: false });
		$img.attr({
			src: imageData.src,
			alt: imageData.alt
		});
	}


	/*
	** Public Properties
	*/
	this.isLoaded = function() {
		return bIsLoaded;
	};

	this.getNoImages = function() {
		return noImages;
	};

	this.getNoLoaded = function() {
		return noLoaded;
	};

	this.getNoErrors = function() {
		return noErrors;
	};

	this.getNoAborted = function() {
		return noAborted;
	};

	this.getIndex = function() {
		return index;
	};


	/*
	** Public Methods
	*/
	this.setIndex = function(i) {
		if ((i < 0) || (i >= noImages)) { i = 0; }
		index = i;
	};

	this.setNextImage = function() {
		this.setIndex(index + 1);
	};

	this.getImage = function() {
		// alert("Returning image: " + (index + 1) + " of " + aImages.length + " images");
		return aImages[index];
	};

	this.getNextImage = function() {
		this.setNextImage();
		return this.getImage();
	};


	this.dispose = function() {
		var i, $img;
		for (i = 0; i < aImages.length; i++) {
			$img = aImages[i];
			$img.removeData("preload").unbind();
		}
	};


	/*
	** Initialse the object
	*/
	for (i = 0; i < images.length; i++) {
		loadImage(images[i]);
	}

};

Abl.UI.ImagePreLoader.defaults = {
	onLoaded: null,
	onError: null,
	onAbort: null
};


Abl.Json.unescape = function(s) {
	s = s.replace(/\\\'/g, "\'");		// Replace single quote
	s = s.replace(/\\\"/g, "\"");		// Replace double quote
	return s;
};
