/*
	file: Utils.js: utility functions
	This is normally included in all pages by header.html
	Requires jQuery. Otherwise general purpose - does not assume SoDash page structure.
	(c) Daniel Winterstein
*/

// Number of occurances of a pattern within an input string
// e.g. "aaahhhh".numberOfOccurances(/a/g) -> 3
// Accepts regexp object or string
// e.g. "aaahhhh".numberOfOccurances("a") also equivalent to above -> 3
// e.g. "adcc".numberOfOccurances("(a|c)[^d]") -> 1
String.prototype.numberOfOccurances = function(pattern) {
	var n = 0;

	if(typeof pattern == "string") {
		pattern = new RegExp(pattern, "g");
	}
	
	this.replace(pattern, function() { n++ });
	
	return n;
};

String.prototype.trim = function() {
	return this.replace(/^\s+|\s+$/g,"");
};

// TODO do we really need/want this?? -- DBW Jan 2012
String.prototype.ltrim = function() {
	return this.replace(/^\s+/,"");
};
//TODO do we really need/want this?? -- DBW Jan 2012
String.prototype.rtrim = function() {
	return this.replace(/\s+$/,"");
};

String.prototype.ucfirst = function() {
	return this.charAt(0).toUpperCase() + this.substr(1, this.length - 1);	
};
//TODO do we really need/want this?? I can't think of any use-cases. -- DBW Jan 2012
String.prototype.lcfirst = function() {
	return this.charAt(0).toLowerCase() + this.substr(1, this.length - 1);	
};

// TODO: Move above string.prototype extensions into below, split utils out into separate XYZ.prototype extension files.
$.extend(String.prototype, {

	//TODO document. Do we really need/want this?? -- DBW Jan 2012
	'lpad' : function(padChar, length) {
		if(padChar.length != 1) return;
						
		var str = this.toString();
		
		if(padChar.length != 1) return;

		while(str.length < length)
			str = padChar + str;

		return str;
	},

	//TODO document. Do we really need/want this?? -- DBW Jan 2012
	'rpad' : function(padChar, length) {
		if(padChar.length != 1) return;
		
		var str = this.toString();														

		while(str.length < length)
			str = str + padChar;

		return str;
	},

	/**
	 * @param offset Must be < 0
	 * @returns the last few (-offset) chars.
	 */
	'substrOffset' : function(offset){
		return this.substr(this.length + offset, this.length);
	}
});

// Array.prototype.map polyfill
if (!Array.prototype.map) {
  Array.prototype.map = function(callback, thisArg) {

    var T, A, k;

    if (this == null) {
      throw new TypeError(" this is null or not defined");
    }

    // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
    var O = Object(this);

    // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
    // 3. Let len be ToUint32(lenValue).
    var len = O.length >>> 0;

    // 4. If IsCallable(callback) is false, throw a TypeError exception.
    // See: http://es5.github.com/#x9.11
    if ({}.toString.call(callback) != "[object Function]") {
      throw new TypeError(callback + " is not a function");
    }

    // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
    if (thisArg) {
      T = thisArg;
    }

    // 6. Let A be a new array created as if by the expression new Array(len) where Array is
    // the standard built-in constructor with that name and len is the value of len.
    A = new Array(len);

    // 7. Let k be 0
    k = 0;

    // 8. Repeat, while k < len
    while(k < len) {

      var kValue, mappedValue;

      // a. Let Pk be ToString(k).
      //   This is implicit for LHS operands of the in operator
      // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
      //   This step can be combined with c
      // c. If kPresent is true, then
      if (k in O) {

        // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
        kValue = O[ k ];

        // ii. Let mappedValue be the result of calling the Call internal method of callback
        // with T as the this value and argument list containing kValue, k, and O.
        mappedValue = callback.call(T, kValue, k, O);

        // iii. Call the DefineOwnProperty internal method of A with arguments
        // Pk, Property Descriptor {Value: mappedValue, Writable: true, Enumerable: true, Configurable: true},
        // and false.

        // In browsers that support Object.defineProperty, use the following:
        // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });

        // For best browser support, use the following:
        A[ k ] = mappedValue;
      }
      // d. Increase k by 1.
      k++;
    }

    // 9. return A
    return A;
  };      
}

// Array.prototype.filter polyfill
if (!Array.prototype.filter)
{
  Array.prototype.filter = function(fun /*, thisp*/)
  {
    var len = this.length;
    if (typeof fun != "function")
      throw new TypeError();

    var res = new Array();
    var thisp = arguments[1];
    for (var i = 0; i < len; i++)
    {
      if (i in this)
      {
        var val = this[i]; // in case fun mutates this
        if (fun.call(thisp, val, i, this))
          res.push(val);
      }
    }

    return res;
  };
}

//IE does not provide an array.indexOf function, so add one
if (!Array.prototype.indexOf) {
	Array.prototype.indexOf = function(elt) {
		var len = this.length;
	
		for (var i = 0; i < len; i++) {
	    		// uses strict equality (no type conversion) for compatibility with Mozilla
		    	if (this[i] === elt) return i;
		}
	
		return -1;
  	};
}

// Define Array.prototype extensions
Array.prototype.insert = function(index, value) {
	Array.prototype.splice.call(this, index, false, value);
};

Array.prototype.remove = function(index) {
	Array.prototype.splice.call(this, index, true);
};

Array.prototype.removeValue = function(value) {
	var index = this.indexOf(value);

	if(index == -1) {
		return false;
	} else {
		this.remove(index);
		return true;
	}
};

Array.prototype.prepend = function(b) {
	Array.prototype.unshift.apply(this, b);
};

Array.prototype.append = function(b) {
	Array.prototype.push.apply(this, b);
};

Array.prototype.max = function() {
    return Math.max.apply(Math, this);
};

Array.prototype.min = function() {
    return Math.min.apply(Math, this);
};



/**
* Function.prototype.bind -- polyfill for older js
*/
if(Function.prototype.bind === undefined) {
	Function.prototype.bind = function() {
		var 
			__method = this, 
			args = Array.prototype.slice.call(arguments), 
			object = args.shift();

		return function() {
			return __method.apply(
				object, args.concat(Array.prototype.slice.call(arguments))
			);
		};
	};
}

/**
* Function.prototype.extend (Richard Assar)
* TODO documentation
*/
Function.prototype.extend = function(fn, before) {
	var method = this;
	
	if(before) {
		return function() {		
			fn.apply(null, arguments);
			return method.apply(null, arguments);
		};		
	} else {
		return function() {	
			var ret = method.apply(null, arguments);
			fn.apply(null, arguments);			
			return ret;
		};	
	}	
};

// Object.keys
if(Object.keys === undefined) {
	Object.keys = function(obj) {
		var ret = new Array();

		for(var prop in obj) {
			if(obj.hasOwnProperty(prop)) {
				ret.push(prop);
			}
		}

		return ret;
	};
}

/**
* Deep clones an object 
* @param [ignore] Optional parameter. An array or string specifying which property/properties to ignore upon recursion
* Richard Assar
*/
Object.clone = function(obj, ignore) {	
	var ret = (obj instanceof Array) ? [] : {};
  
	for (var prop in obj) {
		if(
			obj.hasOwnProperty(prop) && // Ensure property is not deeper in the prototype chain and...
			!(	// Property is not in our ignore string/array (if supplied)
				ignore !== undefined &&
				ignore instanceof Array ?
					ignore.indexOf(prop) > -1 :
					(prop == ignore)
			)		
		) {
			if (obj[prop] !== null && typeof obj[prop] == "object") {    
				ret[prop] = Object.clone(obj[prop], ignore); // Recurse
			} else {
				ret[prop] = obj[prop];	// Leaf
			}
		}
	}
	 
  	return ret;
};

/**
* Modulus operator, corrected to operate correctly with negative numbers
*/
$.extend(Number.prototype, {
	//			
	'clamp' : function(min, max) {
		if(this < min) {
			return min;
		} else if(this > max) {
			return max;
		} else {
			return this;
		}
	},

	//
	'mod' : function(n) {
		return ((this % n) + n) % n;
	}
});

// Date extensions
// TODO These are language & locale specific! Is extending Date the right way to handle them? -- Jan 2012 DBW
$.extend(Date, {
	//
	'msecDay' : 24 * 60 * 60 * 1000,

	//
	'dayNames' : ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],

	//
	'dayNamesShort' : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],				

	//
	'monthNames' : ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],

	//
	'monthNamesShort' : ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],

	//
	'UTCZones' : ["-1200", "-1100", "-1000", "-0930", "-0900", "-0800", "-0700", "-0600", "-0500", "-0430", "-0400", "-0330", "-0300", "-0200", "-0100", "+0000", "+0100", "+0200", "+0300", "+0330", "+0400", "+0430", "+0500", "+0530", "+0545", "+0600", "+0630", "+0700", "+0800", "+0900", "+0930", "+1000", "+1030", "+1100", "+1130", "+1200", "+1245", "+1300", "+1400"],

	//				
	'sameDay' : function(a, b) {
		return (
			a.getDate() == b.getDate() &&
			a.getMonth() == b.getMonth() &&
			a.getFullYear() == b.getFullYear()									
		);						
	},

	'strtotime' : function(string, format) {
		var regExpStr = '';

		var consumers = new Array();
	
		var regExpStr = format.replace(new RegExp("(yy|y|mm|m|MM|M|dd|d|DD|D|hh|h|gg|g|ii|i|a|A|O)", "g"), function(needle, match) {						
			switch(needle) {						
				case 'yy': consumers.push(function(date, string) { date.setFullYear(string); return true; }); return "(\\d{4})";
				case 'y' : consumers.push(function(date, string) { date.setShortYear(string); return true; }); return "(\\d{2})";
				case 'mm': consumers.push(function(date, string) { var val = Number(string); date.setMonth(val.clamp(1, 12) - 1); return val >= 1 && val <= 12; }); return "(\\d{2})";
				case 'm' : consumers.push(function(date, string) { var val = Number(string); date.setMonth(val.clamp(1, 12) - 1); return val >= 1 && val <= 12; }); return "(\\d{1,2})";
				case 'MM': consumers.push(function(date, string) {}); return Date.monthNames.join("|"); // TODO
				case 'M' : consumers.push(function(date, string) {}); return Date.monthNamesShort.join("|"); // TODO
				case 'dd': consumers.push(function(date, string) { var val = Number(string); date.setDate(val.clamp(1, 31)); return val >= 1 && val <= 31; }); return "(\\d{2})";
				case 'd' : consumers.push(function(date, string) { var val = Number(string); date.setDate(val.clamp(1, 31)); return val >= 1 && val <= 31; }); return "(\\d{1,2})";
				case 'DD': consumers.push(function(date, string) {}); return Date.dayNames.join("|"); // TODO
				case 'D' : consumers.push(function(date, string) {}); return Date.dayNamesShort.join("|"); // TODO
				case 'hh': consumers.push(function(date, string) { var val = Number(string); date.setHours(val.clamp(0, 23)); return val >= 0 && val <= 23; }); return "(\\d{2})";
				case 'h' : consumers.push(function(date, string) { var val = Number(string); date.setHours(val.clamp(0, 23)); return val >= 0 && val <= 23; }); return "(\\d{1,2})";
				case 'gg': consumers.push(function(date, string) {}); return "(\\d{2})"; // TODO
				case 'g' : consumers.push(function(date, string) {}); return "(\\d{1,2})"; // TODO
				case 'ii': consumers.push(function(date, string) { var val = Number(string); date.setMinutes(val.clamp(0, 59)); return val >= 0 && val <= 59; }); return "(\\d{2})";
				case 'i' : consumers.push(function(date, string) { var val = Number(string); date.setMinutes(val.clamp(0, 59)); return val >= 0 && val <= 59; }); return "(\\d{1,2})"
				case 'a' : consumers.push(function(date, string) {}); return "(am|pm)"; // TODO
				case 'A' : consumers.push(function(date, string) {}); return "(AM|PM)"; // TODO
				case 'O' : consumers.push(function(date, string) { return date.setUTCTimezone(string); }); return "([+-]\\d{4})";
			}
		});

		regExpStr = '^' + regExpStr + '$';

		var regExp = new RegExp(regExpStr);

		var match = regExp.exec(string);

		if(match == null) return false;

		match.shift();

		var date = new Date(0);

		for(var i = 0; i < match.length; i++) {
			if(!consumers[i](date, match[i])) return false;
		}
						
		return date;
	}
});		

$.extend(Date.prototype, {
	// TODO document. Does this follow a standard? If so -- add a reference. -- DBW Jan 2012
	'format' : function(format) {
		var format = format.replace(new RegExp("('[^']*)?((yy|y|mm|m|MM|M|dd|d|DD|D|hh|h|gg|g|ii|i|a|A|O)|')", "g"), function(needle, match) {
			if(match) return needle;
		
			switch(needle){
				case 'yy': return this.getFullYear();
				case 'y' : return this.getShortYear();
				case 'mm': return ('0' + (this.getMonth()+1)).substrOffset(-2);
				case 'm' : return this.getMonth()+1;
				case 'MM': return Date.monthNames[this.getMonth()];
				case 'M' : return Date.monthNamesShort[this.getMonth()];
				case 'dd': return ('0' + this.getDate()).substrOffset(-2);
				case 'd' : return this.getDate();
				case 'DD': return Date.dayNames[this.getDay()];
				case 'D' : return Date.dayNamesShort[this.getDay()];
				case 'hh': return ('0' + this.getHours()).substrOffset(-2);
				case 'h' : return this.getHours();
				case 'gg': return ('0' + this.get12Hours()).substrOffset(-2);
				case 'g' : return this.get12Hours();
				case 'ii': return ('0' + this.getMinutes()).substrOffset(-2);
				case 'i' : return this.getMinutes();
				case 'a' : return this.getMeridiem();
				case 'A' : return this.getMeridiem().toUpperCase();
				case 'O' : return this.getUTCTimezone();							
			}
		
			return needle;
		}.bind(this));
	
		return format.replace(/'/g, '');
	},				

	//
	'get12Hours' : function() {
		return (this.getHours() == 0) ? 12 : (this.getHours() > 12) ? this.getHours() - 12 : this.getHours();
	},

	//
	'getMeridiem' : function() {
		return (this.getHours() >= 12) ? 'pm' : 'am';
	},

	//
	'getShortYear' : function() {
		return this.getFullYear().toString().substrOffset(-2);
	},

	//
	'getWeek' : function() {
		var test = new Date(this.getFullYear(), 0, 1);
		
		var first = (test.getDay() == 0 || test.getDay() > 4) ? 0 : 1;
		
		return Math.ceil(this.getOrdinal() / 7) + first;
	},

	//
	'getOrdinal' : function() {
		var first = new Date(this.getFullYear(), 0, 1);
		
		return Math.ceil((this.getTime() - first.getTime()) / 86400000);
	},

	//
	'getUTCTimezone' : function() {
		if(this.timeZone !== undefined) return this.timeZone;
		
		var offset = this.getTimezoneOffset();
						
		var mins = Math.abs(offset % 60);
		var hours = Math.abs(offset - (offset % 60)) / 60;
						
		return (offset > 0 ? '-' : '+') + (hours + '' + mins).lpad('0', 4);
	},

	//
	'setUTCTimezone' : function(timeZone) {
		if(Date.UTCZones.indexOf(timeZone) == -1) return false;
		
		this.timeZone = timeZone;

		return true;
	}
});

//
function isArray (obj) {
	//obj.constructor.toString().indexOf(”Array”) != -1;
	return obj instanceof Array;
};

function AssertionError(message) {
	if(message === undefined) message = '';
	
	this.message = message;
};

AssertionError.prototype.toString = function() {
	return "AssertionError: " + this.message;
};

/**
 * assert exp is true
 * @param exp Test for any type of true. 
 * Special case for arrays: an empty array is false. This is for
 * easy sanity checking of jQuery selections.  
 * @param [message] optional output message
 */
function assert(exp, message) {	
	if (exp) {
		// if not an empty array, then done
		if (exp.length === undefined || exp.length != 0) {
			return;
		}
		// Is it really an array? Try for underscore to answer that
		if (_ !== undefined && _.isArray !==undefined) {
			if ( ! _.isArray(exp)) return; /// not an array after all	
			console.log("assert failed on empty array", exp);
		} else {
			return; // assume fine
		}
	}
	
	throw new AssertionError(message);

	if(console.trace) console.trace();
};

/**
 * Input: list of key, value pairs. E.g. asMap(Fields.TAG, "myTag", Fields.XID, "dan@twitter");
 * This is useful 'cos {Fields.TAG, "myTag"} doesn't work.
 * @returns map
 */
function asMap() {
	var mp = {};
	
	for(var i = 0; i < arguments.length; i += 2) {
		mp[arguments[i]] = arguments[i+1];
	}
	
	return mp;
}

var URL_REGEX = /([hf]tt?ps?:\/\/[a-zA-Z0-9_%\-\.,\?&\/=\+'~#!\*:]+[a-zA-Z0-9_%\-&\/=\+])/g;

/**
 * @param url Can be null (returns null)
 * @returns e.g. bbc.co.uk from http://bbc.co.uk/whatever. false if url is not
 * a url. 
 */
function getDomainFromUrl(url) {
	if (!url) return null;
	
	url = "" + url;
	
	var m = url.match(/:\/\/([^\/]+)/);
	
	if (!m) return false;
	
	return m[1];
};

/**
* Matches @you. Use group 2 to get the name *without* the @
*/
var AT_YOU_SIR = /(^|\W)@([\w\-]+)/g;

/**
 * Matches #tag. Use group 2 to get the tag *without* the #
 */
var HASHTAG = /(^|[^&A-Za-z0-9/])#([\w\-]+)/g;

/** Add a GET url argument, triggering a reload */
function addArg(name, value) {
	var url = ""+window.location;
	url = addArg2(url, name, value, [name]);
	window.location = url;
};

function addRemoveArg(name, value, removeNames) {
	var url = ""+window.location;
	removeNames.push(name);
	url = addArg2(url, name, value, removeNames);
	window.location = url;
};

/**Add a set of GET url arguments, triggering a reload*/
function addArgs(args) {
	var url = ""+window.location;
	for(var name in args) {
		url = addArg2(url, name, args[name], [name]);
	}
	window.location = url;
};

/**
 * @param url
 * @param name
 * @param value
 * @param removeNames args to remove. Can be null
 * @returns url with name=value (escaped), and with any of removeNames gone
 */
function addArg2(url, name, value, removeNames) {		
	// Extract the hash param
	var matches = url.match(/#.+/);

	url = url.replace(/#.*$/, '');
	
	var hashParam = (matches != null) ? matches[0] : '';
	
	// There must be a better way!
	var eName = encodeURIComponent(name);
	
	// remove old
	if(!removeNames) removeNames = [name];	
	
	for(var ri = 0; ri<removeNames.length; ri++) {		
		url = removeArg(url, removeNames[ri]);
	}
	
	if(!value) {
		return url;
	}
	
	// prep for adding
	var lc = url.charAt(url.length-1);
	
	if(url.indexOf("?") == -1) {
		url += "?";	
	} else {		
		if(lc != "&" && lc != "?") url += "&";
	}
	
	// add new
	url += eName+"="+encodeURIComponent(value);

	// add hash param back in
	url += hashParam;
	
	return url;
};

/**
 * @returns url without name=?
 */
function removeArg(url, name) {
	var reName = encodeURIComponent(name);	
	url = url.replace("?"+reName, "?&"+reName);	
	
	var oldValRE = new RegExp("&"+reName+"=[^&]*");
	url = url.replace(oldValRE,"");
	
	// clean up trailing ?&
	while(true) {
		var lc = url.charAt(url.length-1);
		if (lc=="?" || lc=="&") url = url.substring(0,url.length-1);
		else break;
	}
	
	url = url.replace("?&", "?");
	
	return url;
};

/** change the rest type of a url, e.g. .html to .json */
function setUrlType(url, type) {
	url = ""+url; // handle window.location
	
	var url2 = url.replace(/\.\w+\?/, type+"?");
	
	if (url2 != url) return url2;	
	url2 = url.replace(/\.\w+$/, type);
	
	if (url2 != url) return url2;
	url2 = url.replace(/\?/, type+"?");
	
	if (url2 != url) return url2;
	
	return url+type;
};

/** Write an email mailto link into a document. Hopefully spam-bot proof.*/
function email(name, domain) {
	document.write("<a href='mailto:"+name+"@"+domain+"'>");
	document.write(name+"@"+domain);
	document.write("</a>");
};

/** 
  name - name of the desired cookie
  Return a string containing the value of specified cookie, or null if cookie does not exist
*/
function getCookie(name) {
	var dc = document.cookie;
	var prefix = name + "=";
	var begin = dc.indexOf("; " + prefix);
	
	if (begin == -1) {	
		begin = dc.indexOf(prefix);
		
		if (begin != 0) return null;
	} 
	else begin += 2;
	
	var end = document.cookie.indexOf(";", begin);
	
	if (end == -1) end = dc.length;
	
	return unescape(dc.substring(begin + prefix.length, end));
};

/** Convert a cookie into a number (otherwise it will be a string) */
function getIntCookie(name) {
	return parseInt( getCookie(name) );
};

/** Create a cookie
	name - name of the cookie
	value - value of the cookie
	These cookies will ask to stay alive for a year.
*/
function setCookie(name, value) {
   var largeExpDate = new Date ();
   
   largeExpDate.setTime(largeExpDate.getTime() + (365 * 24 * 3600 * 1000)); // Last for a year - note that the browser may not respect this.
   
   // Create the cookie string
   var curCookie = name + "=" + escape(value) + "; expires="+largeExpDate.toGMTString()+";";
   
   // Add it to the document
   document.cookie = curCookie; // Note that this *doesn't* re-assign document.cookie (which would delete all existing cookies).
   
   // It magically appends the new cookie to the list.
};

/** Takes an array as input, and returns a randomly chosen element. */
function pickRandom(myarray) {
	var x = Math.floor( Math.random() * myarray.length);
	
	return myarray[x];
};

/** A strong anti-hacking defence: strips out all html tags */
function clean(html) {
	if (html === undefined) return "";
	
	return html.replace(/<[^>]+.*?\/?>/g, ""); // The original expression here was wrong, only removed OPEN tag and not CLOSE tag. Did not have g modifier. - RA
};

/** Toggle the element between visible and hidden.
 return: true if it is now visible, false if it is now hidden */
function toggle(element) {
	if (element.style.display == 'none') {
		element.style.display = 'block';
		return true;
	}
	
	element.style.display = 'none';
	
	return false;
};

/** Set width and height in pixels */
function setSize(elem, width, height) {
	if (width) elem.style.width = width + "px";
	if (height) elem.style.height = height + "px";
};

/** Find x and y positions
//Public domain, by Peter-Paul Koch & Alex Tingle
// Returns: [x,y] */
function getPosition(obj) {
	var curleft = 0;
	
	var curtop = 0;
	
	if(obj.offsetParent) {
		while(true) {		
			curleft += obj.offsetLeft;
			curtop += obj.offsetTop;
			
			if(!obj.offsetParent) break;
			
			obj = obj.offsetParent;
		}
	} else if(obj.x) {
		curleft += obj.x;
		curtop += obj.y;
	}
	
	return [curleft, curtop];
};

/** Set position in pixels
Also stores the x,y coordinates in element.myx, element.myy 
DEPRECATED: use JQuery for preference */
function setPosition(element, x, y) {
	element.myx = x;
	element.myy = y;
	element.style.top = y+'px';
	element.style.left = x+'px';
};

/** A nice readable date */
function getDateString() {
	now = new Date();
	bits = now.toString().split(" ");
	
	// Arrays, which we use to convert numbers from the date object into strings
	days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
	months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
	mystring = days[now.getDay()-1] + ", " + now.getDate() + " " + months[now.getMonth()];
	
	return mystring;
};

/** Parse url arguments 
 * @param [string] Optional, the string to pe parsed, will default to window.location when not provided.
 * @returns a map */
function getUrlVars(string) {
	var url;
	
	if(string === undefined) {
		url = ""+window.location;
	} else {
		url = string;
	}
	
	url = url.replace(/#.*/, '');
	var s = url.indexOf("?");

	if (s==-1 || s==url.length-1) return {};
	
	var varstr = url.substring(s+1);
	var kvs = varstr.split("&");
	var urlVars = {};
	
	for(var i=0; i<kvs.length; i++) {
		var kv = kvs[i];
		var e = kv.indexOf("=");
		
		if (e!=-1 && e!=kv.length-1) {
			k = kv.substring(0,e);
			k = decodeURIComponent(k.replace(/\+/g, " "));
			v = kv.substring(e+1);
			v = decodeURIComponent(v.replace(/\+/g, " "));
			urlVars[k] = v;
		} else {
			urlVars[kv] = '';
		}
	}	
	
	return urlVars;
};

/** Global object which contains all $_GET params **/
window.urlVars = getUrlVars();

/* 
* Determines whether a given element is scrolled into the window boundary. 
* @param elem The element to be checked
* @param noOcclusion When true/defined will enforce that whole element is scrolled onto screen for a true 
*/
function isScrolledIntoView(elem, noOcclusion) {
	if(!elem.is(":visible")) return false;

	var docViewTop = $(window).scrollTop();
	var docViewBottom = docViewTop + $(window).height();

	var elemTop = $(elem).offset().top;
	var elemBottom = elemTop + $(elem).outerHeight();

	var headerHeight = $("#header").outerHeight();

	if(noOcclusion) {
		return (elemBottom <= docViewBottom) && (elemTop >= (docViewTop + headerHeight));
	} else {
		return (elemBottom >= (docViewTop + headerHeight)) && (elemTop <= docViewBottom);
	}	
};

/** Truncates text strings to #words, or #words*6 (whichever is shorter).
 *  Warning: This also compacts whitespace!
 * @param appendString Optional. e.g. "..." This is added if a truncation is performed. **/
function truncateWords(string, numWords, appendString) {
	var words = string.split(/\s+/);
	var nWords = words.length;
	var text = words.splice(0, numWords).join(" ");
	
	// chop if there are very long words (which are generally junk)
	if (text.length > numWords*6) {
		text = text.substring(0, numWords*6);
	}
	
	return text + (appendString !== undefined && nWords > numWords ? appendString : '');	
};

/** 
* Shortens the inner text of any anchor elements in a string 
* @param str The string to be shortened
* @param threshold The threshold value for the length of anchor inner text.
* @param [truncatedLength] If specified will be used instead of the threshold as the length of the resuling inner text
* @author Richard Assar
*/
function shortenURLs(str, threshold, truncatedLength) {
	return str.replace(/<a(.*?)>(.*?)<\/a>/g, function() {
		return '<a' + arguments[1] + '>' + (arguments[2].length > threshold ? arguments[2].substr(0, truncatedLength || threshold) + '...' : arguments[2]) + '</a>';
	});
};

/**
* Parse a URI into its compontents
*/
function parseUri(str) {
	var	o   = parseUri.options,
		m   = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
		uri = {},
		i   = 14;

	while (i--) uri[o.key[i]] = m[i] || "";

	uri[o.q.name] = {};
	uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
		if ($1) uri[o.q.name][$1] = $2;
	});

	return uri;
};

parseUri.options = {
	strictMode: false,
	key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
	q:   {
		name:   "queryKey",
		parser: /(?:^|&)([^&=]*)=?([^&]*)/g
	},
	parser: {
		strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
		loose:  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
	}
};


// Escapes a string, allowing it to be safely used within a regular expression
function regExpEscape(string) {
	return string.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1');
};

/** HTML entity encode/decode
 * @param s
 */
function encodeEntities(s){
	var encodedText = $("<div>").text(s).html();
	encodedText = encodedText.replace(/"/g, '&quot;');
	return encodedText;
};

function decodeEntities(s){
	return $("<div>").html(s).text();
};

/**
 * Encode for use as an html attribute value.
 * ie. escape quotes.
 */
function encodeAttribute(s) {
	if ( ! s) return s;
	s = s.replace(/"/g, '&quot;').replace(/'/g, '&#x27;');
	return s;
};

// jQuery extensions: TODO: Move to separate file (this could get large over time) - RA
(function($) {
	// Make :parents pseudo-selector expression availalbe
	$.expr[':'].parents = function(a, i, m) {
	    return $(a).parents(m[3]).length < 1;
	};

	// Textarea cursor position manipulation
	$.fn.setCursorRange = function(start, end) {
		this.each(function() {
			if (this.setSelectionRange) {
				this.setSelectionRange(start, end);
			} else if (this.createTextRange) {
				var range = this.createTextRange();
		
				range.collapse(true);
				range.moveStart('character', start);				
				range.moveEnd('character', end);
				range.select();
			} else {
				throw "Could not set selection range";
			}
		});

		return this;
	};

	$.fn.setCursorPos = function(pos) {
		return $.fn.setCursorRange.call(this, pos, pos);
	};

	$.fn.getCursorPos = function() {
		var pos = 0;
		
		var input = $(this).get(0);
		
		if (document.selection) { // IE Support
			input.focus();
			
			var sel = document.selection.createRange();
			var selLen = document.selection.createRange().text.length;			
			
			sel.moveStart('character', -input.value.length);			
			
			pos = sel.text.length - selLen;			
		} else if (input.selectionStart || input.selectionStart == '0') { // Firefox support
			pos = input.selectionStart;
		} else {
			throw "Could not get selection range";
		}

		return pos;
	};

	// autoSize. by Richard Assar, infuenced by various stackoverflow answers - minus crud.		
	document.createElement('sizer');
	
	$.fn.autoSize = function() {	
		return this.each(function() {		
			$(this).addClass("autoSize");
			
			$(this)
				.bind("input propertychange paste", function(event) {															
					if(event.type == "propertychange" && event.originalEvent.propertyName != "value") return;
					
					// Size the input									
					var sizerElement = $('<sizer>')
						.css({
							'padding' : 0, 
							'font-size' : $(this).css('font-size'),
							'font-family' : $(this).css('font-family'),
							'font-weight' : $(this).css('font-weight'),
							'letter-spacing' : $(this).css('letter-spacing'),
							'white-space' : 'pre'
						})
						.text($(this).val())
						.appendTo($(document.body));

					var width = sizerElement.innerWidth();

					sizerElement.remove();

					if(width > 0) {
						$(this).width(width + 10);
					} else {
						$(this).css('width', '');
					}		
				})
				.trigger("input"); // Trigger "input", to size based on initial value (set before any user interaction)
		});
	};

	// See: http://stackoverflow.com/questions/2700000/how-to-disable-text-selection-using-jquery
	$.fn.disableSelection = function() {
		return this.each(function() {           
		$(this).attr('unselectable', 'on')
		       .css({
			   '-moz-user-select':'none',
			   '-webkit-user-select':'none',
			   'user-select':'none'
		       })
		       .each(function() {
			   this.onselectstart = function() { return false; };
		       });
		});
	};

	// See: http://stackoverflow.com/questions/1184624/serialize-form-to-json-with-jquery
	$.fn.serializeObject = function() {
	    var o = {};
	    var a = this.serializeArray();
	    $.each(a, function() {
		if (o[this.name] !== undefined) {
		    if (!o[this.name].push) {
		        o[this.name] = [o[this.name]];
		    }
		    o[this.name].push(this.value || '');
		} else {
		    o[this.name] = this.value || '';
		}
	    });
	    return o;
	};
})(jQuery);

