/* Copyright (C) 2005 Splunk Inc. All Rights Reserved. Version 1.0 */


////////////////////////////////////////////////////////////////////////////////
// Constants
////////////////////////////////////////////////////////////////////////////////

// define key codes
// see http://www.quirksmode.org/js/keys.html
var KEY_BACKSPACE	= 8;
var KEY_TAB			= 9;
var KEY_ENTER		= 13;
var KEY_ESCAPE		= 27;
var KEY_SPACE		= 32;
var KEY_DELETE		= 46;
var KEY_UP			= 38;
var KEY_DOWN		= 40;
var KEY_LEFT		= 37;
var KEY_RIGHT		= 39;
var KEY_END			= 35;
var KEY_HOME		= 36;
var KEY_PGUP		= 33;
var KEY_PGDN		= 34;
var KEY_0			= 48;
var KEY_1			= 49;
var KEY_2			= 50;
var KEY_3			= 51;
var KEY_4			= 52;
var KEY_5			= 53;
var KEY_6			= 54;
var KEY_7			= 55;
var KEY_8			= 56;
var KEY_9			= 57;

// these cannot be detected on OSX so use the event properties instead
var KEY_SHIFT		= 16;	// event.shiftKey
var KEY_CTRL		= 17;	// event.ctrlKey || event.metaKey
var KEY_ALT			= 18;	// event.altKey

// special case for modified alphanumerics
var KEY_CTRL_M		= 109;
var KEY_CTRL_M_IE	= 13;
var KEY_CTRL_R		= 114;
var KEY_CTRL_R_IE	= KEY_CTRL_R;



////////////////////////////////////////////////////////////////////////////////
// Global Utilities
////////////////////////////////////////////////////////////////////////////////


util = {
	re : {
		startTimeTerm     :  /\b(starttime::\S+)\b/,
		endTimeTerm       :  /\b(endtime::\S+)\b/,
		indexTerm         :  /\b(index::\S+)\b/,
		indexName         :  /\bindex::(\S+)\b/,
		relStartTimeTerm  :  /(\bstart\S+sago::\S+)/g,
		relEndTimeTerm    :  /(\bend\S+sago::\S+)/g,
		durationTerm      :  /(\bsearchtimespan\S+s::\S+)/,
		timeformatTerm    :  /(\btimeformat::\S+)/,
		peerTerm          :  /\b(server::\S+)\b/,
		deleteTerm        :  /\b(delete::\S+)\b/,
		multiSpace        :  /\s{2,}/g,
		leadingSpace      :  /^\s*/,
		trailingSpace     :  /\s*$/,
		queryLanguage     :  /\bSEARCH (.*)GET (.*)OUTPUT (.*)\b/,
		kvSelect          :  / \| kv \| select .+/	
	},
	_defaultIndex : "default",
	_timezoneOffset : "480",
	_productName : "unknown",
	_instanceName : "unknown",


	getOrigin : function(mozEvent) {
		//return (mozEvent ? mozEvent.target : window.event.srcElement);
		return (document.all) ?  window.event.srcElement : mozEvent.target;
	},

	// some implementations where classNames need to be changed may have multiple class selectors in their classname.  This will add a classname as a class selector, if it is not already present, and without deleting other unrelated classnames that might be present. 
	// returns true if the object already was of this class.  False if it wasnt.  
	// (either way the object is of the class afterward)
	addClass : function(obj, newClassName) {
		var oldClassStr = obj.className;
		// fix for svg and other objects, who dont have a className property
		if (typeof(oldClassStr)=="object") oldClassStr = obj.getAttribute("class");
		
		//TODO. The time for this quick and dirty string stuff is over.  Must be improved
		if ((" " + oldClassStr + " ").indexOf(" " + newClassName + " ")!=-1) return true;

		// fix for svg and other objects, who dont have a className property
		if (typeof(obj.className)=="object") obj.setAttribute("class",oldClassStr + " " + newClassName);
		else obj.className = oldClassStr + " " + newClassName;
		return false;
	},

	// returns true if the object already wasnt of this class.  False if it was.  
	removeClass : function(obj, classNameToRemove) {
		if (!obj || (!obj.className) ) return false;	
		var oldClassStr = obj.className;
		// fix for svg and other objects, who dont have a className property
		if (typeof(oldClassStr)=="object") oldClassStr = obj.getAttribute("class");

		if (oldClassStr.indexOf(classNameToRemove) == -1) return false;
		var newClassStr = (" " + oldClassStr+" ").replace(" " + classNameToRemove + " "," ");
		
		// it passed the quick but dumb test, but not the real one. element wasnt actually of this class.
		if (newClassStr.length == oldClassStr.length  + 2) return false;
		// fix for svg and other objects, who dont have a className property
		if (typeof(obj.className)=="object") obj.setAttribute("class", this.trimString(newClassStr));
		else obj.className = this.trimString(newClassStr);
		return true;
	},

	// tests whether an element is of class classNameToTest. 
	// DOES NOT match in the broader case when the className att contains the string as a part of some other class' name
	isOfClass : function(obj, classNameToTest) {
		if (!obj) return false;	
		var oldClassStr = obj.className;
		if (typeof(oldClassStr)=="object") oldClassStr = obj.getAttribute("class");
			
		if (!oldClassStr || (oldClassStr.indexOf(classNameToTest) == -1))  return false;
		
		// false cases are more numerous, and can be handled quickly
		if (oldClassStr.indexOf(classNameToTest) ==-1) return false;
		// for true case we must be more careful. Using a " " trick instead of regex.
		if (" " + oldClassStr + " ".indexOf(" " + classNameToTest + " ") !=-1) return true;
		else return false;  
	},
	/**
	 * gets lowercase tagname for the given element
	 *
	 * @param {HTMLElement} 
	 */
	 getTagName : function(element) { 
		if (!element || !element.tagName) return "undefined";
		return element.tagName.toUpperCase();
	},
	
	/**
	 * Toggles an object class; either on or off
	 *
	 * @param {boolean} true if class is now set; false if class has been
	 *					removed.
	 */
	toggleClass : function(obj, className) {
		if (!obj) return false;
		if (this.isOfClass(obj, className)) {
			this.removeClass(obj, className);
			return false;
		} else {
			this.addClass(obj, className);
			return true;
		}
	},
	
	hide : function(element) {
		if(typeof(element) == 'string') element = $(element);
		if(!element) return false;
		element.style.display = 'none';
	},

	show : function(element) {
		if(typeof(element) == 'string') element = $(element);
		if(!element) return false;
		element.style.display = 'block';
	},
	
	//TODO rewrite, improve, or drown.
	getValueForKey : function(name, queryString) {
		queryString = "&" + queryString
		var start = queryString.indexOf("&"+name+"=");
		if (start==-1) return false;
		start++;

		// throw away all the stuff before.
		queryString = queryString.substring(start+(name.length) + 1);
		var end = queryString.indexOf("&");
		if (queryString.indexOf("&")==-1) end = queryString.length;
		return queryString.substring(0,end);
	},
	getSelectedValue : function(selectElement, optionalAttributeName) {
		var option = selectElement.options[selectElement.selectedIndex];
		if (optionalAttributeName) return option.getAttribute(optionalAttributeName);
		else if (option.value != "") return option.value;
		else return option.text 
	},


	setSelectedValue : function(selectElement, value, optionalAttributeName) {
		value = value.toLowerCase();
		for (var i=0; i<selectElement.options.length; i++ ) {
			var option = selectElement.options[i];
			if ((optionalAttributeName && option.getAttribute(optionalAttributeName) == value) || !optionalAttributeName && (option.value.toLowerCase() == value)) {
				selectElement.selectedIndex = i;
				selectElement.selectedIndex = i;
				return true;
			}
		}
		return false;
	},
	getEventDate : function(eventRow) {
		try {
			var divNodes = eventRow.getElementsByTagName("DIV")
			var timestampRow = divNodes[0];
			for (var i=0; i<divNodes.length ; i++) {
				if (util.isOfClass(divNodes[i], "timestamp")) {
					timestampRow = divNodes[i].childNodes[0];
					break;
				}	
			}
			
			// our injected headers are interfering with timestamp retrieval.. TODO.  find a better way.
			// implementation the seconde.   This sucks because Date()  by default thinks two digit years are in the 20th century
			foo = new Date(util.getTextContent(timestampRow));
			//D.debug("found timestamp of " + util.getTextContent(timestampRow));
			//D.debug("and parsed it as " + foo);
			if (foo.getYear() < 75)  foo.setYear(foo.getYear() + 2000);
			return foo
			// implementation the firste.   This sucked because calendar.js parseDate is lame and doesnt work with 100% of dates.  (it occasionally thinks seconds value is the year value)
			/*
			var foo = Date.parseDate(util.getTextContent(timestampRow), window.SEARCH_RESULTS_TIME_FORMAT);
			D.debug("found timestamp of " + util.getTextContent(timestampRow));
			D.debug("and parsed it as " + foo.print(window.SEARCH_RESULTS_TIME_FORMAT));
			return foo;
			*/
			
		}
		catch (e) {
			return new Date()
		}
		
	},
	getLeft : function(element) {
		var walker = element;
		var totalOffsetLeft = 0;
		while (walker && walker.offsetParent) {
			
			totalOffsetLeft += walker.offsetLeft;
			walker = walker.offsetParent;
		}
		return totalOffsetLeft;
	},
	getRight : function(element) {
		return util.getLeft(element) + element.offsetWidth;
	},
	getTop : function(element) {
		var walker = element;
		var totalOffsetTop = 0;
		
		while (walker && walker.offsetParent) {
			//debugMsg(util.getTagName(walker)+ "." + walker.className + ", id " + walker.getAttribute("id") + " offsetTop " + walker.offsetTop)
			totalOffsetTop += walker.offsetTop;
			walker = walker.offsetParent;
		}
		
		return totalOffsetTop;
	},
	
	
	/**
	 * Makes a deep copy of a DOM node and copies it as a child of the
	 * destination (target) node.
	 *
	 * @param {DOMElement} target The destination node in which to insert the
	 * 				new elements.
	 * @param {DOMElement} sourceReference The element node to deep copy.
	 *
	 */
	copyIntoDocument : function(target, sourceReference) {
		
		var targetNode = (typeof(target) == "object") ? target : $(target);
		
		targetNode.innerHTML = '';  
		// it's like a quarter second faster to just append on the inplace reference,
		// rather than clone the whole thing like we were doing before.
		if (document.all)  targetNode.innerHTML = sourceReference.outerHTML;
		else {
			var newFloatingClonedNode;
			newFloatingClonedNode = sourceReference.cloneNode(true);
			targetNode.appendChild(newFloatingClonedNode);
			delete newFloatingClonedNode;
		}
		/*
		// this implementation is 19.8% faster, by my somewhat painstaking calculation,  than cloning and appending the node.
		//but doesnt work when the content is heavily nested.   in these cases it starts enforcing XHTML rules like not allowing nested labels. 
		// which unfortunately wreaks some havoc with our product because we use label as a generic flavorless 'segment' tag, and nest them a lot.
		var targetNode = (typeof(target) == "object") ? target : $(target);
		targetNode.innerHTML = sourceReference.innerHTML;
		return true;	
		*/

	},
	
	setCookie : function(cookieName,cookieValue,nDays) {
		var today = new Date();
		var expire = new Date();
		if (nDays==null || nDays==0) nDays=1;
		expire.setTime(today.getTime() + 3600000*24*nDays);
		document.cookie = cookieName+"="+escape(cookieValue) + ";expires="+expire.toGMTString();
	},
	getCookie : function(cookieName) {
		var cookieString=""+document.cookie;
		var index1=cookieString.indexOf(cookieName);
		if (index1==-1 || cookieName=="") return ""; 
		var index2=cookieString.indexOf(';',index1);
		if (index2==-1) index2=cookieString.length; 
		return unescape(cookieString.substring(index1+cookieName.length+1,index2));
	},
	// a little global function that returns a reasonably cross-browser reference to the originating object of the given mozEvent. 
	getEvent : function(mozEvent) {
		return mozEvent || window.event;
	},

	setLeft : function(element, left) {
		(document.all) ? element.style.posLeft = left : element.style.left = left + "px";
	},
	setTop : function(element, top) {
		(document.all) ? element.style.posTop = top : element.style.top = top + "px";
	},
	setWidth : function(element, width) {
		(document.all) ? element.style.posWidth = width: element.style.width = width+ "px";
	},
	setHeight : function(element, height) {
		(document.all) ? element.style.posHeight = height: element.style.height = height+ "px";
	},
	firstIsParentOf : function(first, second) {
		var walker = second;
		while (walker.parentNode && walker !=first) {
			walker = walker.parentNode;
		}
		if (walker==first) return true;
		else return false;
	},
	trimString : function(sInString) {
		sInString = sInString.replace(util.re.leadingSpace, "" );// strip leading
		sInString = sInString.replace("\n","");
		return sInString.replace(util.re.trailingSpace, "" );     // strip trailing
	},
	toTitleCase : function(sInString) {
		var ls = sInString.toLowerCase();
		//turn it into an array by splitting at spaces
		var la = ls.split(" ");
		//loop through word array
		for (var i = 0; i < la.length; i++ ) {
			//replace first letter with uppercase version
			la[i] = la[i].charAt(0).toUpperCase() + la[i].slice(1);
		}
		//rejoin words and return the new string
		return la.join(" ");
	},
	containsTerm : function(needle, haystack) {
		needleComparator = " " + this.trimString(needle.toLowerCase()) + " ";	
		haystackComparator = " " + this.trimString(haystack.toLowerCase()) + " ";
		if (haystackComparator.indexOf(needleComparator) !=-1) {
			return true;
		}
		else return false;
	},
	removeTerm : function(needle, haystack) {
		if (!this.containsTerm(needle, haystack)) return haystack;
		var loweredNeedle = needle.toLowerCase();
		var loweredHaystack = " " + haystack.toLowerCase() + " ";
		// finds indexes of the offending word,  and slices it out.  Need to do this because we dont want to change haystack's casing.
		var replacementStartIndex = loweredHaystack.indexOf(" " + loweredNeedle + " ")
		if (replacementStartIndex != -1) {
			var replacementEndIndex = replacementStartIndex + loweredNeedle.length + 1
			var newTermString = haystack.substring(0,replacementStartIndex)  + haystack.substring(replacementEndIndex, haystack.length)
			return this.trimString(newTermString);
		}
		else return this.trimString(haystack);
	},
	// NEEDS TO BE OPTIMIZED. THIS IS ACTUALLY SURPRISINGLY EXPENSIVE ON FIREFOX.
	// Should just push cached values to util onload and onresize?
	getWindowDimString : function() {
		if (!is.opera && this.innerHeight) return "self.inner";
		else if (!is.opera && document.documentElement && document.documentElement.clientHeight) return "document.documentElement.client";
		else if (document.body) return "document.body.client";
	},
	getWindowHeight : function() {
		return eval(this.getWindowDimString() + "Height");
	},
	getWindowWidth : function() {
		return eval(this.getWindowDimString() + "Width");
	},
	belowTheFold : function(element) {
		try {        return (this.getTop(element) > this.getWindowHeight());      }
		catch (e) {  return false;  }
	},

	getIframeDocument : function() {
		var iframe = $("queryFetcher");
		var iframeDoc = (iframe.contentWindow || iframe.contentDocument);
		if (iframeDoc.document) return iframeDoc.document;
		return iframeDoc;
	},
	getBuildNumber : function() {
		return window.SPLUNK_BUILD_NUMBER
	},
	getStaticPath : function() {
		return "/static_oxiclean_"+ util.getBuildNumber();
	},
	isNumeric : function(candidate) {
		if ("" + parseInt(candidate) == "NaN") return false;
		else return true;
	},
	undoXMLEscaping : function(xmlSafeStr) {
		var s=xmlSafeStr.replace(/\&lt;/g,"<");
		s=s.replace(/\&gt;/g,">");
		s=s.replace(/\&amp;/g,"&");
		s=s.replace(/\&quot;/g,"\"");
		return s.replace(/\&apos;/g,"'");
	},
	getTextContent : function(element)  {
		
		if (!element) {
			throw new Error('util.getTextContent - null element passed');
		}
		
		// we are careful, cause the element reference might get garbage-collected and be sorta kinda working when it gets here, but have no content.  
		// strange cases, but reproducible. Seems like DOM3 stuff gets killed first. 
		try {
			if (element.textContent) {
				return element.textContent;
			} else if (element.innerText) {
				return element.innerText;
			} else if (element.text) {
				return element.text;
			}
		}
		// right now the only known cases are if the elements are being cleared 
		catch (e) {
			if ((element.childNodes.length == 1 ) && element.childNodes[0].nodeType == 3) {
				return element.innerHTML;
			}
			else {
				// TODO:
			}
		}
		return ""
	},
	scrollToId : function(id, center) {
		var selectedEvent = $(id);
		var resultsScroller = $("resultsScroller");
		var scrollerOffset = util.getTop(resultsScroller);
		var windowHeight = util.getWindowHeight();
		var newScrollTop = util.getTop(selectedEvent) - scrollerOffset;
		if (center) newScrollTop -= ((windowHeight- scrollerOffset)/2);

		resultsScroller.scrollTop = Math.round(newScrollTop)
	},
	
		
	setSelectionRange : function(input, selectionStart, selectionEnd) {
	  if (input.setSelectionRange) {
		input.focus();
		input.setSelectionRange(selectionStart, selectionEnd);
	  }
	  else if (input.createTextRange) {
		var range = input.createTextRange();
		range.collapse(true);
		range.moveEnd('character', selectionEnd);
		range.moveStart('character', selectionStart);
		range.select();
	  }
	},
	/*
	escapeBarInEllipses : function(text, open, close) {
    
		var re = new RegExp("(?![\\\\])(\\" + open + '|\\' + close + ')');
		text = text.replace(/\\\"/, "__ELVIS__");
		D.debug("argument " + text);

	
		var sepSubs = text.split(re);

		alert(sepSubs);
		var result = ""
		var depth = 0
		for (var i=0; i<sepSubs.length; i++) {
			var val = sepSubs[i]
			// handle quotes and other ellipses that only have depth of 1. alternate between 0, 1
			if ((val == open) && (val == close)) {
				depth = (depth+1) % 2;
			} else if (val == open) {
				depth += 1;
			} else if (val == close) {
				depth -= 1;
			}
				
			if (depth == 0) {
				result += val;
			} else {
				result += val.replace("|", "XXX");
			}
		}
		result = result.replace(/__ELVIS__/, "\\\"");
		D.debug("result " + result);
		return result;
	},
	*/

	
	setDefaultIndex : function(newIndex)	{this._defaultIndex = newIndex;},
	getDefaultIndex : function()			{return this._defaultIndex;},
	setServerTimezone : function(offset)	{this._timezoneOffset = 	offset;},
	getServerTimezone : function()			{return this._timezoneOffset;},
	setInstanceName : function(name)		{this._instanceName = name;},
	getInstanceName : function()			{return this._instanceName;},
	setProductName : function(name)			{this._productName = name},
	getProductName : function()				{return this._productName},
	
	/**
	 * Completely stops all further event behavior.
	 *
	 */
	extinguishAllEvents: function(evt) {
		if(window.event) window.event.cancelBubble = true;
		if(evt.stopPropagation) evt.stopPropagation();
		if(evt.preventDefault) evt.preventDefault();
	},
	cancelEventBubbling : function(mozEvent) {	
		if (document.all)  window.event.cancelBubble = true;
		else if (mozEvent) {
			if (mozEvent.stopPropagation) mozEvent.stopPropagation();
			else D.warn("event has no stopPropagation method. cutil.cancelEventBubbling failed.");
		}
		else D.warn("cancelEventBubbling was called but took no action.");
	},
	isLastDayOfMonth : function(oDate)  {
		oLocalDateCopy = new Date(oDate)
		var month = oLocalDateCopy.getMonth();
		var day   = oLocalDateCopy.getDate();
		oLocalDateCopy.setDate(oLocalDateCopy.getDate() + 1);
		D.debug("toConciseString - isLastDayOfMonth " + oLocalDateCopy.getMonth() + " == " + month);
		return (oLocalDateCopy.getMonth() != month );
	}, 
	/**
	 * Takes a given DOM node, and recursively appends 'idSuffix' to the 'id'
	 * attribute of any element that already has one assigned.
	 *
	 */
	renameNodeIds: function(node, idSuffix) {
		if (node.id) node.id += idSuffix;
		for (var i=0, l=node.childNodes.length; i<l; i++) {
			if (node.childNodes[i].hasChildNodes) {
				util.renameNodeIds(node.childNodes[i], idSuffix);
			} else if (node.childNodes[i].id) {
				node.childNodes[i].id += idSuffix;
			}
		} 
	}, 
	makeTermSafeForSearchLanguage : function(termStr, forceOuterQuotes) {
		// replace backslash chars with backslash-escaped backslash chars.
		if (termStr.indexOf("\\") !=-1) termStr = termStr.replace(/\\/g, '\\\\');
		// replace quote chars with backslash-escaped quote chars. 
		if (termStr.indexOf("\"") !=-1) termStr = termStr.replace(/\"/g, '\\"');
		
		// vars for clarity - this is a weird case of catching unmatched opening or unmatched closing parens. (not both)
		var hasOpeningParen = termStr.charAt(0)!="(";
		var hasClosingParen = termStr.charAt(termStr.length -1)!=")";

		// furthermore, if it contains either opening or closing square bracket or pipe anywhere,  wrap it in quotes. (escaping square brackets doesnt work)
		if ((termStr.indexOf("[") !=-1  ) || (termStr.indexOf("]") !=-1) || (termStr.indexOf("|") !=-1)  || (hasOpeningParen != hasClosingParen)) {
			termStr = '"' + termStr + '"';
		}
		// if the client requested that the term should be explicitly double-quoted, 
		// and we havent already quoted it automatically.
		if (forceOuterQuotes && (termStr.charAt(0)!='"' || filterValue.charAt(termStr.length-1)!='"')) {
			termStr = '"' + termStr + '"';
		}
		return termStr
	}
}


// shorthand for document.getElementById
function $(element) {
	return document.getElementById(element);
	/*
	var elements = new Array();

	for (var i = 0; i < arguments.length; i++) {
		var element = arguments[i];
		if (typeof element == 'string')
			element = document.getElementById(element);

		if (arguments.length == 1)
			return element;

		elements.push(element);
	}
	return elements;
	*/
}

function getElementsByClassName(node, classname)
{
    var a = [];
    var re = new RegExp('\\b' + classname + '\\b');
    var els = node.getElementsByTagName("*");
    for (var i=0,j=els.length; i<j; i++)
        if (re.test(els[i].className))a.push(els[i]);
    return a;
}


/**
 * Serializes the property names of an object literal into a space-delimited
 * string.
 *
 * @param {object} hash The object literal to serialize
 * @return {string} The space-delimited string of key names
 *
 */
function prop2str(hash) {
	var o = '';
	for (x in hash) o += " " + x;
	return o.substring(1);
}


/**
 * Converts an object literal to an encoded querystring key/value string.
 *
 */
function propToQueryString(dictionary, encodeValue) {
	if (encodeValue === null) encodeValue = true;
	var o = [];
	var val;
	for (prop in dictionary) {
		val = '' + dictionary[prop];
		if (encodeValue)
			o.push(encodeURIComponent(prop) + '=' + encodeURIComponent(dictionary[prop]));
		else
			o.push(prop + '=' + dictionary[prop]);
	}
	return o.join('&');
}

////////////////////////////////////////////////////////////////////////////////
// Missing Javascript prototype methods
////////////////////////////////////////////////////////////////////////////////

if (!String.prototype.trim) {
	String.prototype.trim = function (delim) {
		if (!delim) delim = '';
		return this.replace(new RegExp("^[\\s" + delim + "]+"),'').replace(new RegExp("[\\s" + delim + "]+$"), '');
	}
}


/**
 * Checks if string is a member of either an array, or object literal (keys).
 *
 *
 * @param {Object} iterable The array or object literal to inspect
 * @param {Boolean} useStrictEquality Indicates whether or not to use strict 
 * 				equality comparison when inspecting arrays
 * @return {Boolean} True if string is a member, false otherwise
 *
 */
if (!String.prototype.isMemberOf) {
	String.prototype.isMemberOf = function (iterable, useStrictEquality) {
		if (!iterable) return false;
		if(iterable instanceof Array) {
			for(var i=0,l=iterable.length; i<l; i++) {
				if(useStrictEquality && this === iterable[i]) return true;
				else if(this == iterable[i]) return true;
			}
		} else {
			if(iterable.hasOwnProperty && iterable.hasOwnProperty(this)) return true;
		}
		return false;
	}
}


if (!String.prototype.startswith) {
	String.prototype.startswith = function (substring) {
		if(substring == null) throw new Error('String.startswith - substring is null');
		if(!substring) return true;
		if(this.length < substring.length) return false;
		return (this.substring(0,substring.length) == substring);
	}
}

if (!String.prototype.endswith) {
	String.prototype.endswith = function (substring) {
		if(substring == null) throw new Error('String.endswith - substring is null');
		if(!substring) return true;
		if(this.length < substring.length) return false;
		return (this.substring(this.length-substring.length) == substring);
	}
}

if (!Array.prototype.indexOf) {
	Array.prototype.indexOf = function (obj, fromIndex) {
		if (fromIndex == null) {
			fromIndex = 0;
		} else if (fromIndex < 0) {
			fromIndex = Math.max(0, this.length + fromIndex);
		}
		for (var i = fromIndex; i < this.length; i++) {
			if (this[i] === obj)
				return i;
		}
		return -1;
	};
}


/**
 * Same as Array.push(), but doesn't append if the incoming object is null
 *
 */
if (!Array.prototype.lazyPush) {
	Array.prototype.lazyPush = function (obj) {
		if(obj != null) this.push(obj);
	};
}

/**
 * Returns new array with elements from toRemove subtracted from array
 *
 */
if (!Array.prototype.subtract) {
	Array.prototype.subtract = function(toRemove) {
		var o = this.concat();
		for(var i=0,l=toRemove.length; i<l; i++) {
			for(var j=0; j<o.length; j++) {
				if(toRemove[i] == o[j]) {
					o.splice(j,1);
					j--;
				}
			}
		}
		return o;
	}
}

/**
 * Returns new array with elements from toIntersect intersected with array
 * TODO: optimize
 */
if (!Array.prototype.intersect) {
	Array.prototype.intersect = function(toIntersect) {
		var o = [];
		for(var i=0,l=toIntersect.length; i<l; i++) {
			if(this.indexOf(toIntersect[i]) > -1) o.push(toIntersect[i]);
		}
		return o;
	}
}


/**
 * Removes all elements in an array that match an object
 *
 */
if (!Array.prototype.remove) {
	Array.prototype.remove = function(toRemove) {
		for(var i=this.length-1; i>=0; i--) {
			if(this[i] == toRemove) {
				this.splice(i, 1);
			}
		}
	}
}	
	
	
/**
 * Returns the index of the first matched element
 *
 */
if (!Array.prototype.indexOf) {
	Array.prototype.indexOf = function(toMatch) {
		for(var i=0,l=this.length; i<l; i++) {
			if(this[i] == toMatch) {
				return i;
			}
		}
		return -1;
	}
}


/**
 * Adds an element to the array, if element does not exist.
 *
 * @param {object} toAdd The object to add to the array.
 * @return {boolean} True if object was added; false if object already exists
 *
 */
if (!Array.prototype.add) {
	Array.prototype.add = function(toAdd) {
		if(this.indexOf(toAdd) > -1) return false;
		return this.push(toAdd);
	}
}


/**
 * Parses a Splunk-issued datetime string and returns a Date() object
 *
 */
if (!Date.fromSplunkString) {
	Date.fromSplunkString = function(dateString) {
		var re = /(\d{1,2})\/(\d{1,2})\/(\d{4}):(\d{2}):(\d{2}):(\d{2})/;
		var parts = re.exec(dateString);
		if(!parts) return null;
		return new Date(parts[3], parseInt(parts[1],10)-1, parts[2], parts[4], parts[5], parts[6]);
	}
}

var $A = function(iterable) {
  if (!iterable) return [];
  if (iterable.toArray) {
    return iterable.toArray();
  } else {
    var results = [];
    for (var i = 0, length = iterable.length; i < length; i++)
      results.push(iterable[i]);
    return results;
  }
}

/**
 * Black magic
 *
 */
Function.prototype.bind = function() {
  var __method = this, args = $A(arguments), object = args.shift();
  return function() {
    return __method.apply(object, args.concat($A(arguments)));
  }
}


/**
 * Returns a deep copy of an object.  Assumes that any object with toJSONString
 * is a Storage() objecct.
 *
 */
function clone(obj) {
	if(typeof(obj) != 'object') return obj;
	if(obj instanceof Array) return obj.concat();
	if(!(obj)) return obj;
	if(obj.toJSONString) {
		var o = new Storage();
	} else {
		var o = {}
	}
	for (prop in obj) {
		//TODO. verify that recursive deep-copying isnt complete insane.
		if(obj.hasOwnProperty(prop)) o[prop] = clone(obj[prop]);
	}
	return o;
}


////////////////////////////////////////////////////////////////////////////////
// Browser Detection
////////////////////////////////////////////////////////////////////////////////

// general purpose Browser detection object.
function Browser(){
	var ieRegex = /\bMSIE (\d\.\d)\b/;
	this.dom = document.getElementById?1:0;
	this.ie =  document.all?1:0;
	if (ieRegex.test(navigator.appVersion)) {
		this.ieVersion = parseFloat((navigator.appVersion.match(ieRegex)[1]));
	}
	this.mac = (navigator.appVersion.indexOf('Mac') == -1)?0:1;
	this.ua = navigator.userAgent.toLowerCase();
	this.safari = (this.ua.indexOf("safari") != -1)?1:0;
	this.opera  = (this.ua.indexOf("opera") != -1)?1:0;
}
Browser.prototype.checkSupported = function() {
	var alertString = ""
	if (!this.dom) alertString += 'Splunk requires Internet Explorer 6, Mozilla or Firefox to work properly.';
	
	else if (this.ie) alertString += 'Splunk 3.0 beta currently does not support Internet Explorer.  We recommend using Firefox. The released version of Splunk 3.0 will support IE 6 & 7.';

	else if (this.opera) alertString += 'Splunk does not support Opera yet. We support Mozilla, Netscape and Firefox browsers.';

	else if (this.safari)  alertString += 'Splunk does not support Safari yet. We support Mozilla, Netscape and Firefox browsers for Macs.';

	if (alertString != "") alert(alertString) ;

}
Browser.prototype.onLoadHandler = function() {
	if (this.mac && $("outerWrapper")) {
		var wrapperDiv = $("outerWrapper")
		util.addClass(wrapperDiv, "macCSSOverride");
	}
}

// init browser detection script
window.is = new Browser();
window.is.onLoadHandler();


////////////////////////////////////////////////////////////////////////////////
// TimeRange objects
////////////////////////////////////////////////////////////////////////////////

/**
 * General purpose object to hold both absolute and relative time ranges.
 */
function TimeRange(startTime, endTime) {
	if (startTime) this.startTime = startTime;
	else this.startTime = false;
	if (endTime)   this.endTime = endTime;
	else this.endTime   = false;
}
/**
 * returns the startTime as a JS Date object.
 *
 */
TimeRange.prototype.getStartTime = function() {
	return this.startTime;}
/**
 * returns the endTime as a JS Date object.
 *
 */
TimeRange.prototype.getEndTime = function() {
	return this.endTime;}
/**
 * Serializes the timeRange. Generally for debug statements. For human-readable purposes, use toConciseString()
 *
 */
TimeRange.prototype.toString = function() {
	var str = "";
	if (this.startTime) {
		str += this.startTime.print("\n%m.%d.%Y %H:%M:%S,  ");
	}
	if (this.endTime) {
		str += this.endTime.print("%m.%d.%Y %H:%M:%S");
	}
	if (this.relativeTerm) {
		str += " " + this.relativeTerm;
	}
	return str;
}
/**
 * returns a compact, natural language description of the range.
 *
 */
TimeRange.prototype.toConciseString = function() {
	var duration = this.getDuration();
	
	var valueUnitArr = this.getRelativeValueAndUnit();
	// duration is -1 in both relative modes, and in 'ALL_TIME' mode.
	if ((duration == -1)) {
		if (!valueUnitArr[0] && !valueUnitArr[1]) {
			return "over all time"
		}
		else if (valueUnitArr[0] == 1) {
			return "in the past " + valueUnitArr[1];
		}
		else {
			return "in the past " + valueUnitArr[0] + " " + valueUnitArr[1] + "s";
		}
	}
	else {
		var zipperTogetherUpTo  = false;
		//zipperTogetherUpTo = second  | minute | hour | day | month
		var rangeIsSingleUnitOf = false;
		//rangeIsSingleUnit = second | minute | hour | day | month
		var rangeIsIntegerUnitsOf = false;

		if (this.startTime.getFullYear() == this.endTime.getFullYear()) {
			zipperTogetherUpTo = "year";
			if (this.startTime.getMonth() == this.endTime.getMonth()) {
				//D.debug("toConciseString - A")
				zipperTogetherUpTo = "month";
				// within one day
				if (this.startTime.getDate() == this.endTime.getDate()) {
					//D.debug("toConciseString - B")
					zipperTogetherUpTo = "day";
					// within one hour
					if (this.startTime.getHours() == this.endTime.getHours()) {
						//D.debug("toConciseString - C")
						zipperTogetherUpTo = "hour";
						// within one minute
						if (this.startTime.getMinutes() == this.endTime.getMinutes()) {
							//D.debug("toConciseString - D")
							zipperTogetherUpTo = "minute";
							// ONE SINGLE SECOND
							if (this.startTime.getSeconds() == this.endTime.getSeconds()) {
								// this is redundant case to set this var, but lets do it anyway for consistency
								//D.debug("toConciseString - E")
								zipperTogetherUpTo = "second"
								// No further analysis is necessary as the timeline only goes down to seconds.
								rangeIsSingleUnitOf = "second"
							}
							// x number of seconds within 1 minute, x > 1
							else {
								// ONE SINGLE MINUTE
								if (this.startTime.getSeconds()==0 && this.endTime.getSeconds() == 59) {
									//D.debug("toConciseString - F")
									rangeIsSingleUnitOf = "minute"
								}
								else {
									//D.debug("toConciseString - G")
									rangeIsIntegerUnitsOf = "second"
								}
							}
						}
						else {
							// ONE SINGLE HOUR
							if (this.startTime.getMinutes()==0 && this.endTime.getMinutes() == 59) {
								//D.debug("toConciseString - H")
								rangeIsSingleUnitOf = "hour"
							}
							else if (this.startTime.getSeconds()==0 && this.endTime.getSeconds() == 59) {
								//D.debug("toConciseString - I")
								rangeIsIntegerUnitsOf = "minute";
							}
						}
					}
					else {
						// ONE SINGLE DAY
						if (this.startTime.getHours()==0 && this.endTime.getHours() == 23) {
							//D.debug("toConciseString - J")
							rangeIsSingleUnitOf = "day"
						}
						else if (this.startTime.getMinutes() == 0 && this.endTime.getMinutes() == 59) {
							//D.debug("toConciseString - K")
							rangeIsIntegerUnitsOf = "hour";
						}
					}
				}
				else {
					// ONE SINGLE MONTH
					if (this.startTime.getDate()==1 && util.isLastDayOfMonth(this.endTime)) {
						//D.debug("toConciseString - L")
						rangeIsSingleUnitOf = "month"
					}
					else if (this.startTime.getHours() == 0 && this.endTime.getHours() == 23) {
						//D.debug("toConciseString - M")
						rangeIsIntegerUnitsOf = "day"
					}
				}
			}
			//same year, not same month.
			else {
				//integer number of months, bounded by a single year.
				if (this.startTime.getDate()==1 && util.isLastDayOfMonth(this.endTime)) {
					rangeIsIntegerUnitsOf = "month"
				}
			}
		}
		if (rangeIsSingleUnitOf && ! rangeIsIntegerUnitsOf) rangeIsIntegerUnitsOf = rangeIsSingleUnitOf;

		//D.debug("toConciseString - rangeIsSingleUnitOf " + rangeIsSingleUnitOf);
		//D.debug("toConciseString - rangeIsIntegerUnitsOf " + rangeIsIntegerUnitsOf);
		//D.debug("toConciseString - zipperTogetherUpTo " + zipperTogetherUpTo);
		/* step 1
			easy cases of range = 1 exact day, 1 exact hour, etc...
		*/
		switch (rangeIsSingleUnitOf) {
			case "month" :
				return this.startTime.print("during %B %Y");
			case "day" : 
				return this.startTime.print("on %A %B %e %Y");
			case "hour" :
				return this.startTime.print("at %l %p on %A %B %e %Y");
			case "minute" :
				return this.startTime.print("at %H:%M %p %A %B %e %Y");
			case "second" :
				return this.startTime.print("at %H:%M:%S %p on %A %B %e %Y");
			default : 
				//D.debug("toConciseString - this range is not a single unit of anything");
		}
		/*  step 2 harder weirder corner cases where the range satisfies 
		  a)  it is an integer number of X where x is months | days | hours | minutes | seconds
		  b)  the range does not span a boundary of X's parent Y.  
		*/
		endTimePatch = new Date(this.endTime);
		//endTimePatch.setSeconds(endTimePatch.getSeconds()+1);
		switch (rangeIsIntegerUnitsOf) {
			case "month" :
				return this.startTime.print("%B")  + endTimePatch.print(" through %B %e %Y");
			case "day" :
				return this.startTime.print("%B %e")  + endTimePatch.print(" through %B %e %Y");
			case "hour" :
				return this.startTime.print("%l %p")  + endTimePatch.print(" through %l %p %A %B %e %Y");
			case "minute" :
				return this.startTime.print("%l:%M %p")  +  endTimePatch.print(" through %l:%M:%S %p on %A %B %e %Y");
			// "second" case is useless, redundant, confusing, you name it.  We fall through to step 3
			//case "second" : 
			//	return this.startTime.print("from %H:%M:%S %p") +  endTimePatch.print(" through %H:%M:%S %p on %B %e %Y");
			default :
				//D.debug("toConciseString - this range is not an integer number of anything.");
		}
		/* step 3
			some cases of folding common units together in the display. 
		*/
		switch (zipperTogetherUpTo) {
			// redundant, nonsense case.
			//case "second" :
			//	return this.startTime.print("between %H:%M  and ") +  this.endTime.print("%H:%M on %B %e %Y");
			case "year" :
			case "month" :
				return this.startTime.print("between %l:%M:%S %p %B %e and ") +  this.endTime.print("and %l:%M:%S %p %B %e %Y");
			case "day" : 
			case "hour" :
			case "minute" :
				return this.startTime.print("between %l:%M:%S %p and ") +  this.endTime.print("%l:%M:%S %p on %A %B %e %Y");
			default : 
				//D.debug("toConciseString - this range start and endtime shared no unit values in common.");
		}
		/* step 3
			total fallback. No special cases detected.  Print times with full precision.
		*/
		return this.startTime.print("between %l:%M:%S %p %B %e %Y") +  this.endTime.print(" and %l:%M:%S %p %B %e %Y");
	}
}
/**
 * public method useful for the moment. Used to split the relative term into a value and a unit.  Obviously wont generalize well to relativeTerm properties that are more than one term.
 *
 */
TimeRange.prototype.getRelativeValueAndUnit = function() {
	if (!this.relativeTerm) {
		return false
	}
	var unit  = false;
	var value = false;
	var unitsToTestFor = ["seconds", "minutes", "hours", "days", "months"]
	
	for (var i=0;i<unitsToTestFor.length;i++) {
		var u = unitsToTestFor[i];
		if (this.relativeTerm.indexOf(u)!=-1) {
			unit = u.substring(0, u.length-1);
		}
	}
	try {
		value = this.relativeTerm.substring(this.relativeTerm.indexOf("::")+2);
	}
	catch (e) { 
		D.error("error within TimeRange. Failed to extract relative term value");
		return false;
	}
	return [value,unit];
}
/**
 * very useful method, that is given a REFERENCE to a termlist. It modifies the list in place, pulling out time terms that it finds, and using them to populate itself. 
 * Returns true if the given terms contained any time terms. false otherwise.
 */
TimeRange.prototype.absorbTimeTerms = function(termList) {
	var relativeTerms = [];
	var lengthBefore = termList.length;
	for (var i=termList.length-1; i>=0; i--) {
		var term = termList[i];
		if (term.match(util.re.endTimeTerm)) {
			var endDate = term.match(util.re.endTimeTerm)[0].replace("endtime::","");
			this.endTime = Date.parseDate(endDate, window.SEARCH_TERM_TIME_FORMAT);
			
			termList.splice(i,1);
		}
		else if (term.match(util.re.startTimeTerm)) {
			var startDate = term.match(util.re.startTimeTerm)[0].replace("starttime::","");
			this.startTime = Date.parseDate(startDate, window.SEARCH_TERM_TIME_FORMAT);
			termList.splice(i,1);
		}
		// until we can actually widgetize endtimes and durations throughout the system, lets leave them alone.
		//else if (term.match(util.re.relStartTimeTerm) || term.match(util.re.relEndTimeTerm) || term.match(util.re.durationTerm)) {
		else if (term.match(util.re.relStartTimeTerm)) {
			relativeTerms.push(term);
			termList.splice(i,1)
		}
	}
	if (relativeTerms.length > 0) {
		this.relativeTerm = relativeTerms.join(" ");
	}
	return lengthBefore != termList.length;
}
/**
 * Copies a new TimeRange instance and returns.  Useful for when you want to pass a TimeRange property, but you dont entirely trust the receiver to leave it unchanged. 
 */
TimeRange.prototype.copy = function() {
	var previousStr = this.toString();
	var range = new TimeRange(this.startTime, this.endTime);
	if (this.relativeTerm) range.relativeTerm = this.relativeTerm;
	if (previousStr != range.toString()) {
		D.error("TimeRange.copy - something went horribly wrong");
	}
	return range;
}
/**
 * returns true if the given range is contained by or equal to this instance.  Doesnt at the moment have any notion of relative ranges.
 */
TimeRange.prototype.containsRange = function(range) {
		// if this is a range over all time, then it contains all time ranges. (including other "all time" ranges )
		if (!this.relativeTerm && !this.getStartTime() && !this.getEndTime()) return true;
		if (this.equalToRange(range)) return true;
		// Currently relative ranges will ALWAYS claim to contain absolute ranges.
		if (this.relativeTerm  && !range.relativeTerm) return true;
		// trivial but expensive case of them being identical.  happens a lot. 
		if (this.getSearchTerms().join(" ") == range.getSearchTerms().join(" ")) return true;
		
		if (this.getStartTime() > range.getStartTime())  return false;
		else if (this.getEndTime() < range.getEndTime()) return false;
		
		return true;
	try {}
	catch(e) {
		D.error("exception in TimeRange.containsRange");
		D.debug("exception thrown by \nrange=" + range.toString() + "\nthis=" + this.toString());
		return false;
	}
}
/**
 * returns true if the given range is exactly equal to this instance.
 */
TimeRange.prototype.equalToRange = function(range) {
	if (this===range) return true;
	if (this.relativeTerm != range.relativeTerm) return false;
	if (typeof(this.startTime) != typeof(range.startTime)) return false;
	if (typeof(this.endTime) != typeof(range.endTime)) return false;
	if (this.startTime && this.startTime.getTime() != range.getStartTime().getTime()) return false;
	else if (this.endTime && this.endTime.getTime() != range.getEndTime().getTime()) return false;
	return true;
}

/** 
 * @returns -1 if the timeRange isnt governed by a relative searchTerm at all. Otherwise an integer representing the number of milliseconds between startTime and endTime.
 */
TimeRange.prototype.getDuration = function() {
	if (this.relativeTerm) return -1;
	else if (this.endTime && this.startTime) {
		return this.endTime - this.startTime;
	}
	else return -1;
}
/** 
 * @returns the starttime:: and endtime:: search terms that match the object's time values.
 */
TimeRange.prototype.getSearchTerms = function() {
	var timeTerms = [];
	if (this.relativeTerm) timeTerms.push(this.relativeTerm);
	else {
		if (this.startTime) timeTerms.push(this.startTime.print("starttime::" + window.SEARCH_TERM_TIME_FORMAT));
		if (this.endTime)	timeTerms.push(this.endTime.print("endtime::" + window.SEARCH_TERM_TIME_FORMAT));
	}
	//D.debug("TimeRange.getSearchTerms - returning " + timeTerms);
	return timeTerms;
}



////////////////////////////////////////////////////////////////////////////////
// Hashtable object
////////////////////////////////////////////////////////////////////////////////

function Hash() {
	this._hash = {};
}

Hash.prototype = {
	get: function(key) {
		if(this._hash.hasOwnProperty(key)) return this._hash[key];
		else return null;
	},
	set: function(key, value) {
		this._hash[key] = value;
	},
	remove: function(key) {
		return (delete this._hash[key]);
	},
	containsKey: function(key) {
		return this._hash.hasOwnProperty(key);
	},
	keys: function() {
		var o = [];
		for (var x in this._hash) {
			if(this._hash.hasOwnProperty(x)) o.push(x);
		}
		return o;
	}
}
Hash.prototype.put = Hash.prototype.set;
Hash.prototype.getKeys = Hash.prototype.keys;


/**
 * This is an alternate version of Hash(), which is JSON compatible; we probably
 * should switch everything over to this soon.
 *
 */
function Storage() {
}
Storage.prototype.append = function(objectLiteral) {
	for(key in objectLiteral) {
		this[key] = objectLiteral[key];
	}
}
Storage.prototype.toJSONString = function () {
    var a = ['{'],  // The array holding the text fragments.
        b,          // A boolean indicating that a comma is required.
        k,          // The current key.
        v;          // The current value.

    function p(s) {

// p accumulates text fragment pairs in an array. It inserts a comma before all
// except the first fragment pair.

        if (b) {
            a.push(',');
        }
        a.push(k.toJSONString(), ':', s);
        b = true;
    }

// Iterate through all of the keys in the object, ignoring the proto chain.

    for (k in this) {
        if (this.hasOwnProperty(k)) {
            v = this[k];
            switch (typeof v) {

// Serialize a JavaScript object value. Ignore objects that lack the
// toJSONString method. Due to a specification error in ECMAScript,
// typeof null is 'object', so watch out for that case.

            case 'object':
                if (v) {
                    if (typeof v.toJSONString === 'function') {
                        p(v.toJSONString());
                    }
                } else {
                    p("null");
                }
                break;

            case 'string':
            case 'number':
            case 'boolean':
                p(v.toJSONString());

// Values without a JSON representation are ignored.

            }
        }
    }

// Join all of the fragments together and return.

    a.push('}');
    return a.join('');
};

////////////////////////////////////////////////////////////////////////////////
// Update checker functions
////////////////////////////////////////////////////////////////////////////////

// return true if it tried to create a scriptlet based on the url found in search.xml
// return false if no url was found. 
function createUpdateCheckerScriptlet(checkerLocation) {
	if (!$("footer")) return false;
	var scriptlet = document.createElement("SCRIPT");
	scriptlet.type = "text/javascript";
	bannerURLSegments = [];
	// 'pro' | 'free'
	bannerURLSegments[0] = (util.isOfClass($("outerWrapper"),"proVersion")) ? "pro" : "free";
	// '2.1b2' | '2,0' etc.. 
	bannerURLSegments[1] = $("footer").getAttribute("versionNumber");
	// 'home' | 'login'
	bannerURLSegments[2] = checkerLocation
	// 'prod' | 'trial'
	bannerURLSegments[3] = $("footer").getAttribute("installType");
	// 'basic' | 'black' | various user created skins..
	bannerURLSegments[4] = skinManager.getActiveSkinTitle();
	var updateCheckerBaseURL = "";
	try {
		var serviceURLContainer = $("serviceURLs");
		updateCheckerBaseURL = serviceURLContainer.getAttribute("updateCheckerBaseURL");
	}
	catch (e) {
		//D.error("could not find updateCheckerBaseURL");
	}
	// only send the updateChecker to the url if it IS a url.  If it's '0'  or 'no', send nothing.
	if ((updateCheckerBaseURL.indexOf("http://")!=-1) || (updateCheckerBaseURL.indexOf("https://")!=-1)) {
		scriptlet.src = updateCheckerBaseURL + bannerURLSegments.join("/");	
		document.getElementsByTagName('head')[0].appendChild(scriptlet);
		return true;
	}
	else {
		return false;
	}
}


function displaySpot(spotParams) {
	if (window.cannotConnectTimeout) clearTimeout(window.cannotConnectTimeout);

	var url = false;
	if ("url" in spotParams) url = spotParams["url"];
	
	if (url) {
		$("bannerContainer").style.display = "block";
		$("updateChecker").src = url;
	}
	else return false;
	
	updateCheckerIframe = $("updateChecker");
	if ("height" in spotParams)  updateCheckerIframe.style.height = spotParams["height"] + "px";
	
	if ("failsafeCSSText" in spotParams) updateCheckerIframe.style.cssText= "" + spotParams["failsafeCSSText"];
	return true;
}

function possiblyFallBackToCannotConnectMessage() {
	var updateCheckerIframe = $("updateChecker")
	if ($("bannerContainer").style.display == "block") {
		// this means the quickdraw js request succeeded, so quickdraw has the ball now. we're done. it'll call displaySpot, which will point the iframe at quickdraw
		return false;
	}
	var updateCheckerIframe = $("updateChecker");
	$("bannerContainer").style.display = "block";
	updateCheckerIframe.src = updateCheckerIframe.getAttribute("failureSrc");
	updateCheckerIframe.style.visibility = "visible";
	updateCheckerIframe.style.height = "95px";
	updateCheckerIframe.style.width  = "728px";
	updateCheckerIframe.style.frameborder = "0";
	return true;
}
function setupLandingPageUpdateChecker(){
	if (createUpdateCheckerScriptlet("home")) {
		window.cannotConnectTimeout = setTimeout('possiblyFallBackToCannotConnectMessage()', 5000);
	}
}


function setupLoginUpdateChecker(){
	window.skinManager = new SkinManager();
	product = ((util.isOfClass($("outerWrapper"),"proVersion")) ? "pro" : "free");
	//this prevent js error in free version where there is no loginForm
	if (product=="pro") document.loginForm.usr.focus();
	if (createUpdateCheckerScriptlet("login")) {
		window.cannotConnectTimeout = setTimeout('possiblyFallBackToCannotConnectMessage()', 5000);
		return true;
	}
	else {
		return false;
	}
}



////////////////////////////////////////////////////////////////////////////////
// Debug/timing functions
////////////////////////////////////////////////////////////////////////////////

// init the logging level
if(util.getCookie('LOG_LEVEL')) {
	window.LOG_LEVEL = parseInt(util.getCookie('LOG_LEVEL'), 10);
} else {
	window.LOG_LEVEL = 10;
}

// init html debug popup window
if(util.getCookie('LOG_POPUP')) {
	window.LOG_POPUP = true;
}

var gTimingStartedAt = new Date().getTime();

/**
 * Internal. Outputs a debug message into a popup window.
 *
 * Usage: 
 *
 */
function debugMsg(msg, timingArgument){
	
	if(!window.LOG_POPUP) return;
	
	var now = new Date();
	var hours = now.getHours();
	var minutes = now.getMinutes();
	var seconds = now.getSeconds();
	var ms = now.getMilliseconds();
	
	
	if (!top.debugWindow || top.debugWindow.closed){	
		top.debugWindow = window.open("", "debugWindow", 'scrollbars, resizable, width=800,height=700');
		top.debugWindow.document.write('<div style="position:fixed;top:0;left:0;z-index:10;width:100%;height:18px;font:11px sans-serif;background:#eee;padding:4px"><a href="javascript:document.write(\'<br /><hr /><br />\');window.scrollBy(0,100 )">Mark</a></div><p>&nbsp;</p><pre>');
		//top.debugWindow.moveTo(0,0);
	}

	var duration = -1;
	if (timingArgument == "startTiming") {
		gTimingStartedAt = now;
	}
	

	top.debugWindow.document.write('<span style="font-family:Monaco,Courier Bold,Courier New,Courier,monospace; color: #444; font-size: 10px;">');
	top.debugWindow.document.write('<br />[' +  hours + ':' + minutes + ':' + seconds + '.' + ms + '] ');
	top.debugWindow.document.write( msg );
	if (timingArgument == "endTiming") {
		top.debugWindow.document.write(" -- TOOK " + (now.getTime() - gTimingStartedAt) + " ms");
		gTimingStartedAt = now.getTime();
	}
	top.debugWindow.document.write('</span>');
	
	top.debugWindow.scrollBy( 0,100 );

	if (!window.justFocused){
		//top.debugWindow.focus();
		//top.debugWindow.opener.focus();
		window.justFocused = true;
		window.setTimeout('window.justFocused=false', 1000);
	}
	return true;
}

/**
 * Outputs messages to the logging framework.  This is meant to be used with the
 * Firebug extension for Firefox.
 *
 */
var D = {
	msg: function(level, m, t) {
		if (window.console) {
			if (window.console.debug) eval("console." + level + "(m);");
			//else if(window.console.log && typeof(m) == 'string') console.log(level.toUpperCase() + ' ' + m.replace(/\n+/g, ' '));
		}
		return debugMsg(level.toUpperCase() + '  ' + m,t);
	},
    debug:  function(m,t) { if(window.LOG_LEVEL>0) return false; return this.msg('debug  ', m,t); },
    debugj: function(m,t) { if(window.LOG_LEVEL>5) return false; return this.msg('debug  ', m,t); },
    info:   function(m,t) { if(window.LOG_LEVEL>10) return false; return this.msg('info  ', m,t); },
    warn:   function(m,t) { if(window.LOG_LEVEL>20) return false; return this.msg('warn  ', m,t); },
    error:  function(m,t) { if(window.LOG_LEVEL>30) return false; return this.msg('error  ', m,t); },
	group: function(m) { if(window.console && window.console.debug) window.console.group(m); },
	groupEnd: function(m) { if(window.console && window.console.debug) window.console.groupEnd(); },
    r:      function(m,t) {
	    var o = "[Property listing]\n";
    	for (var i in m) o += "\t" + i + " => " + m[i] + "\n";
    	if (window.console) console.debug(o);
    	return debugMsg('DEBUG  ' + o,t);
    },
    a:      function(m,t) {
	    var o = "[Array listing]\n";
    	for (var i=0,l=m.length;i<l;i++) o += "\t" + i + " => " + m[i] + "\n";
    	if (window.console) console.debug(o);
    	return debugMsg('DEBUG  ' + o,t);
    }
}

/**
 * Sets the logging level (via Firebug) for the current browser.  Logging levels
 * are as follows:
 * 0 = DEBUG
 * 5 = DEBUG (jv)
 * 10 = INFO
 * 20 = WARN
 * 30 = ERROR
 *
 */
function setLoggingLevel(level) {
	if(isNaN(parseInt(level))) return false;
	window.LOG_LEVEL = level;
	util.setCookie('LOG_LEVEL', level, 1000);
	return level;
}


/**
 * Turns the popup debug window on and off
 */
function setPopupLogger(showPopup) {
	if(showPopup) {
		window.LOG_POPUP = true;
		util.setCookie('LOG_POPUP', '1', 1000);
		return true;
	} else {
		delete window.LOG_POPUP;
		util.setCookie('LOG_POPUP', '0', -1000);
		return false;
	}
}

////////////////////////////////////////////////////////////////////////////////
// Window-level event handler functions
////////////////////////////////////////////////////////////////////////////////

var mouseWheelScroller  = function(mozEvent) {
	var st = scrollingPanelManager.resultsScroller.scrollTop + (mozEvent.detail * 12);
	scrollingPanelManager.resultsScroller.scrollTop = st < 0 ? 0 : st;
	mozEvent.preventDefault();
	if (window.popupLayerManager) window.popupLayerManager.closeAll();
	if (document.body.scrollTop != 0) document.body.scrollTop = "0px";
}


////////////////////////////////////////////////////////////////////////////////
// Global window onload/onunload hooks
////////////////////////////////////////////////////////////////////////////////

/**
 * Holds functions to run global onload
 *
 */
window.onloads = new Array();

/**
 * Adds an onload action to the global window onload event
 *
 */
window.addOnload = function(somethingToDo) {
	window.onloads[window.onloads.length] = somethingToDo;
}

/**
 * Links the custom window.onloads array into the global window.onload
 * event handler.
 *
 * this could be made to respect embedded onload handlers on the body tag, but 
 * currently it will not. It will overwrite them. 
 *
 */
window.onload = function() {
	for (var i=0; i<window.onloads.length; i++) {
		//D.debug("RUNNING ONLOAD FOR " + window.onloads[i]);
		eval(window.onloads[i]);
	}
	return true
}


/**
 * Defines the global window onunload action; cleans up memory to avoid
 * leaks.
 *
 * be careful.  IE does have some memory leak problems.  generally not an issue 
 * because sessions aren't constantly loaded
 * 
 */
window.onunload = function() {
	
	var queryIframe = $("queryFetcher");

	// the default one
	if (window.queryOnload && queryIframe && queryIframe.removeEventListener) {
		queryIframe.removeEventListener('load',queryOnload,false);
	}
	
	queryOnload = null;
	if ($("queryFetcher")) $("queryFetcher").onload = null;
	document.onkeypress = document.mousemove = document.mouseup = document.onclick = null;

	if(window.stateManager) {
		removeEvent(document, "keypress", stateManager.handleOnKeyPress);
	}
	if (window.searchController) {
		window.searchController.detach();
	}
	
	if (window.typeAheadManager) {
		removeEvent(document,"keypress",  typeAheadManager.handleKeyPress);
		for (key in window.typeAheadManager.typeAheadControllers) {
			var controller = window.typeAheadManager.typeAheadControllers[key];
			controller.typeAheadLayer = null;
			controller.field.manager = null;
			controller.field.onkeypress = null;
			controller.field.onkeyup = null;
			controller.field.onkeydown = null;
			controller.field = null;
			controller = null;
		}
	}
	if (window.popupLayerManager) {
		removeEvent(document, "keypress",  popupLayerManager.handleKeyPress);
		removeEvent(document, "mousemove", popupLayerManager.handleMouseMove);
		removeEvent(document, "mouseup",   popupLayerManager.handleMouseUp);
		var ulElements = document.getElementsByTagName("UL");
		for (var i=0;i<ulElements.length;i++) {
			var elt = ulElements[i];
			// not all ul elements are selectable modal menus
			if (util.isOfClass(elt,"modalOptions")) {
				elt.onclick = null;
			}
		}
	}
	
	var h5Elements = document.getElementsByTagName("H5");
	for (var i=0;i<h5Elements.length;i++) {
		var elt = h5Elements[i];
		if (util.isOfClass(elt, "popperUpper")) {
			elt.onclick = null;
		}
	}
	window.scrollingPanelManager	= null;
	window.domElementCloner		    = null;
	window.popupLayerManager		= null;
	window.queryFactory				= null;
	window.typeAheadManager		 	= null;
	window.stateManager			    = null;
	window.timelineManager			= null;
	window.typeAheadManager			= null;
}

/**
 * Collect methods to fire when window is resized
 *
 */
window.onresizes = new Array();
window.addOnresize = function(somethingToDo) {
	window.onresizes[window.onresizes.length] = somethingToDo;
}
window.onresize= function() {
	for (var i=0; i<window.onresizes.length; i++) {
		eval(window.onresizes[i]);
	}
}



////////////////////////////////////////////////////////////////////////////////
// environment init functions
////////////////////////////////////////////////////////////////////////////////

/**
* Alias attachEvent(IE5+) as addEventListener(DOM-2).
*/
if (!document.addEventListener && document.attachEvent)
document.addEventListener = function(type, listener, useCapture){this.attachEvent("on"+type, listener);};

/**
* Alias detachEvent(IE5+) as removeEventListener(DOM-2).
*/
if (!document.removeEventListener && document.detachEvent)
document.removeEventListener = function(type, listener, useCapture){this.detachEvent("on"+type, listener);};


function testTimeRangePrinting() {
	var toCompare = new Array();
	var f = "%m/%d/%Y, %H:%M:%S"
	// 1 SECOND
	toCompare[0] = [
		new TimeRange(Date.parseDate("01/05/2007, 04:02:00",f), Date.parseDate("01/05/2007, 04:02:01",f))
		,"between 4:02:00 AM and 4:02:01 AM on Friday January 5 2007"];
	//within 1 minute, not bounded on minute boundaries
	toCompare[1] = [
		new TimeRange(Date.parseDate("01/05/2007, 04:02:03",f), Date.parseDate("01/05/2007, 04:07:07",f))
		,"between 4:02:03 AM and 4:07:07 AM on Friday January 5 2007"];
	//within 1 minute, bounded on minute boundaries
	toCompare[2] = [
		new TimeRange(Date.parseDate("01/05/2007, 04:02:00",f), Date.parseDate("01/05/2007, 04:16:59",f))
		,"4:02 AM through 4:16:59 AM on Friday January 5 2007"];
	//not within 1 minute, Not bounded on minute boundaries
	toCompare[3] = [
		new TimeRange(Date.parseDate("01/05/2007, 04:01:56",f), Date.parseDate("01/05/2007, 04:16:59",f))
		,"between 4:01:56 AM and 4:16:59 AM on Friday January 5 2007"];
	// not within 1 minute, bounded on minute boundaries
	toCompare[4] = [
		new TimeRange(Date.parseDate("01/05/2007, 03:59:56",f), Date.parseDate("01/05/2007, 04:01:36",f))
		,"between 3:59:56 AM and 4:01:36 AM on Friday January 5 2007"];
	//within 1 hour, bounded on hour boundaries
	toCompare[5] = [
		 new TimeRange(Date.parseDate("01/05/2007, 04:00:00",f), Date.parseDate("01/05/2007, 04:59:59",f))
		, "at 4 AM on Friday January 5 2007"];
	// within 1 hour, not bounded on hour boundaries
	toCompare[6] = [
		 new TimeRange(Date.parseDate("01/05/2007, 04:13:00",f), Date.parseDate("01/05/2007, 04:56:59",f))
		, "4:13 AM through 4:56:59 AM on Friday January 5 2007"];
	//not within 1 hour, bounded on hour boundaries
	toCompare[7] = [
		 new TimeRange(Date.parseDate("01/05/2007, 03:00:00",f), Date.parseDate("01/05/2007, 05:59:59",f))
		, "3 AM through 5 AM Friday January 5 2007"];
	//not within 1 hour, not bounded on hour boundaries
	toCompare[8] = [
		 new TimeRange(Date.parseDate("01/05/2007, 03:00:00",f), Date.parseDate("01/05/2007, 06:05:59",f))
		, "between 3:00:00 AM and 6:05:59 AM on Friday January 5 2007"];
	//within 1 day, bounded on day boundaries
	toCompare[9] = [
		 new TimeRange(Date.parseDate("01/04/2007, 00:00:00",f), Date.parseDate("01/04/2007, 23:59:59",f))
		, "on Thursday January 4 2007"];
	//within 1 day, not bounded on day boundaries
	toCompare[10] = [
		 new TimeRange(Date.parseDate("01/04/2007, 00:00:00",f), Date.parseDate("01/04/2007, 21:59:59",f))
		, "12 AM through 9 PM Thursday January 4 2007"];
	//not within 1 day, bounded on hour boundaries
	

	var failureStr = ""
	for (var i=0;i<toCompare.length;i++) {
		var printed = toCompare[i][0].toConciseString();
		var ideal   = toCompare[i][1]
		
		if (printed != ideal) {
			var failure = "\n\ntest #" + i + " failed - \n"+ printed + " (not equal to) \n " + ideal;
			//alert(failure)
			failureStr += failure;
		}
	}	
	return failureStr;
}


////////////////////////////////////////////////////////////////////////////////
// Event attachment/detachment methods
////////////////////////////////////////////////////////////////////////////////

// credit to John Resig, illustrious winner of addEvent, removeEvent contest.  (this is his winning entry)
function addEvent (obj, type, fn ) {
	if (obj.addEventListener)
		obj.addEventListener( type, fn, false );
	else if (obj.attachEvent) {
		obj["e"+type+fn] = fn;
		obj[type+fn] = function() { obj["e"+type+fn]( window.event ); }
		obj.attachEvent( "on"+type, obj[type+fn] );
	}
}
function removeEvent (obj, type, fn) {
	if (obj.removeEventListener)
		obj.removeEventListener( type, fn, false );
	else if (obj.detachEvent) {
		obj.detachEvent( "on"+type, obj[type+fn] );
		obj[type+fn] = null;
		obj["e"+type+fn] = null;
	}
}


////////////////////////////////////////////////////////////////////////////////
// JSON - this is the reference implementation from JSON.org, without the
// object.prototype.toJSONString method
////////////////////////////////////////////////////////////////////////////////

Array.prototype.toJSONString = function () {
    var a = ['['],  // The array holding the text fragments.
        b,          // A boolean indicating that a comma is required.
        i,          // Loop counter.
        l = this.length,
        v;          // The value to be stringified.

    function p(s) {

// p accumulates text fragments in an array. It inserts a comma before all
// except the first fragment.

        if (b) {
            a.push(',');
        }
        a.push(s);
        b = true;
    }

// For each value in this array...

    for (i = 0; i < l; i += 1) {
        v = this[i];
        switch (typeof v) {

// Serialize a JavaScript object value. Ignore objects thats lack the
// toJSONString method. Due to a specification error in ECMAScript,
// typeof null is 'object', so watch out for that case.

        case 'object':
            if (v) {
                if (typeof v.toJSONString === 'function') {
                    p(v.toJSONString());
                }
            } else {
                p("null");
            }
            break;

// Otherwise, serialize the value.

        case 'string':
        case 'number':
        case 'boolean':
            p(v.toJSONString());

// Values without a JSON representation are ignored.

        }
    }

// Join all of the fragments together and return.

    a.push(']');
    return a.join('');
};
Array.prototype.dedupe = function() {
	var tmp = new Array();
	for (i=0;i<this.length;i++){
		if (tmp.indexOf(this[i])==-1){
			tmp[tmp.length]=this[i];
		}
	}
	return tmp;
}

Boolean.prototype.toJSONString = function () {
    return String(this);
};


Date.prototype.toJSONString = function () {

// Ultimately, this method will be equivalent to the date.toISOString method.

    function f(n) {

// Format integers to have at least two digits.

        return n < 10 ? '0' + n : n;
    }

    return '"' + this.getFullYear() + '-' +
            f(this.getMonth() + 1) + '-' +
            f(this.getDate()) + 'T' +
            f(this.getHours()) + ':' +
            f(this.getMinutes()) + ':' +
            f(this.getSeconds()) + '"';
};


Number.prototype.toJSONString = function () {

// JSON numbers must be finite. Encode non-finite numbers as null.

    return isFinite(this) ? String(this) : "null";
};


(function (s) {

// Augment String.prototype. We do this in an immediate anonymous function to
// avoid defining global variables.

// m is a table of character substitutions.

    var m = {
        '\b': '\\b',
        '\t': '\\t',
        '\n': '\\n',
        '\f': '\\f',
        '\r': '\\r',
        '"' : '\\"',
        '\\': '\\\\'
    };


    s.parseJSON = function (filter) {

// Parsing happens in three stages. In the first stage, we run the text against
// a regular expression which looks for non-JSON characters. We are especially
// concerned with '()' and 'new' because they can cause invocation, and '='
// because it can cause mutation. But just to be safe, we will reject all
// unexpected characters.

        try {
            if (/^("(\\.|[^"\\\n\r])*?"|[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t])+?$/.
                    test(this)) {

// In the second stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.

                var j = eval('(' + this + ')');

// In the optional third stage, we recursively walk the new structure, passing
// each name/value pair to a filter function for possible transformation.

                if (typeof filter === 'function') {

                    function walk(k, v) {
                        if (v && typeof v === 'object') {
                            for (var i in v) {
                                if (v.hasOwnProperty(i)) {
                                    v[i] = walk(i, v[i]);
                                }
                            }
                        }
                        return filter(k, v);
                    }

                    j = walk('', j);
                }
                return j;
            }
        } catch (e) {

// Fall through if the regexp test fails.

        }
        throw new SyntaxError("parseJSON");
    };


    s.toJSONString = function () {

// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can simply slap some quotes around it.
// Otherwise we must also replace the offending characters with safe
// sequences.

        if (/["\\\x00-\x1f]/.test(this)) {
            return '"' + this.replace(/([\x00-\x1f\\"])/g, function(a, b) {
                var c = m[b];
                if (c) {
                    return c;
                }
                c = b.charCodeAt();
                return '\\u00' +
                    Math.floor(c / 16).toString(16) +
                    (c % 16).toString(16);
            }) + '"';
        }
        return '"' + this + '"';
    };
})(String.prototype);


 

// @name      The Fade Anything Technique
// @namespace http://www.axentric.com/aside/fat/
// @version   1.0-RC1
// @author    Adam Michela
var Fat = {
	make_hex : function (r,g,b) 
	{
		r = r.toString(16); if (r.length == 1) r = '0' + r;
		g = g.toString(16); if (g.length == 1) g = '0' + g;
		b = b.toString(16); if (b.length == 1) b = '0' + b;
		return "#" + r + g + b;
	},
	fade_all : function ()
	{
		var a = document.getElementsByTagName("*");
		for (var i = 0; i < a.length; i++) 
		{
			var o = a[i];
			var r = /fade-?(\w{3,6})?/.exec(o.className);
			if (r)
			{
				if (!r[1]) r[1] = "";
				if (o.id) Fat.fade_element(o.id,null,null,"#"+r[1]);
			}
		}
	},
	fade_element : function (id, fps, duration, from, to) 
	{
		if (!fps) fps = 30;
		if (!duration) duration = 3000;
		if (!from || from=="#") from = "#fff5bf";
		if (!to) to = this.get_bgcolor(id);
		
		var frames = Math.round(fps * (duration / 1000));
		var interval = duration / frames;
		var delay = interval;
		var frame = 0;
		
		if (from.length < 7) from += from.substr(1,3);
		if (to.length < 7) to += to.substr(1,3);
		
		var rf = parseInt(from.substr(1,2),16);
		var gf = parseInt(from.substr(3,2),16);
		var bf = parseInt(from.substr(5,2),16);
		var rt = parseInt(to.substr(1,2),16);
		var gt = parseInt(to.substr(3,2),16);
		var bt = parseInt(to.substr(5,2),16);
		
		var r,g,b,h;
		while (frame < frames)
		{
			r = Math.floor(rf * ((frames-frame)/frames) + rt * (frame/frames));
			g = Math.floor(gf * ((frames-frame)/frames) + gt * (frame/frames));
			b = Math.floor(bf * ((frames-frame)/frames) + bt * (frame/frames));
			h = this.make_hex(r,g,b);
		
			setTimeout("Fat.set_bgcolor('"+id+"','"+h+"')", delay);

			frame++;
			delay = interval * frame; 
		}
		setTimeout("Fat.set_bgcolor('"+id+"','"+to+"')", delay);
	},
	set_bgcolor : function (id, c)
	{
		var o = document.getElementById(id);
		if(o) o.style.backgroundColor = c;
	},
	get_bgcolor : function (id)
	{
		var o = document.getElementById(id);
		while(o)
		{
			var c;
			if (window.getComputedStyle) c = window.getComputedStyle(o,null).getPropertyValue("background-color");
			if (o.currentStyle) c = o.currentStyle.backgroundColor;
			if ((c != "" && c != "transparent") || o.tagName == "BODY") { break; }
			o = o.parentNode;
		}
		if (c == undefined || c == "" || c == "transparent") c = "#FFFFFF";
		var rgb = c.match(/rgb\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/);
		if (rgb) c = this.make_hex(parseInt(rgb[1]),parseInt(rgb[2]),parseInt(rgb[3]));
		return c;
	}
}



/*
 * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
 * Digest Algorithm, as defined in RFC 1321.
 * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for more info.
 */

/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance   */
var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */

/*
 * These are the functions you'll usually want to call
 * They take string arguments and return either hex or base-64 encoded strings
 */
function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));}
function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));}
function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));}
function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); }
function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); }
function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); }

/*
 * Perform a simple self-test to see if the VM is working
 */
function md5_vm_test()
{
  return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
}

/*
 * Calculate the MD5 of an array of little-endian words, and a bit length
 */
function core_md5(x, len)
{
  /* append padding */
  x[len >> 5] |= 0x80 << ((len) % 32);
  x[(((len + 64) >>> 9) << 4) + 14] = len;

  var a =  1732584193;
  var b = -271733879;
  var c = -1732584194;
  var d =  271733878;

  for(var i = 0; i < x.length; i += 16)
  {
    var olda = a;
    var oldb = b;
    var oldc = c;
    var oldd = d;

    a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
    d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
    c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
    b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
    a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
    d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
    c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
    b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
    a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
    d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
    c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
    b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
    a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
    d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
    c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
    b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);

    a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
    d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
    c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
    b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
    a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
    d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
    c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
    b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
    a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
    d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
    c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
    b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
    a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
    d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
    c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
    b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);

    a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
    d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
    c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
    b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
    a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
    d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
    c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
    b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
    a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
    d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
    c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
    b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
    a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
    d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
    c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
    b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);

    a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
    d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
    c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
    b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
    a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
    d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
    c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
    b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
    a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
    d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
    c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
    b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
    a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
    d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
    c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
    b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);

    a = safe_add(a, olda);
    b = safe_add(b, oldb);
    c = safe_add(c, oldc);
    d = safe_add(d, oldd);
  }
  return Array(a, b, c, d);

}

/*
 * These functions implement the four basic operations the algorithm uses.
 */
function md5_cmn(q, a, b, x, s, t)
{
  return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
}
function md5_ff(a, b, c, d, x, s, t)
{
  return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t)
{
  return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t)
{
  return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t)
{
  return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}

/*
 * Calculate the HMAC-MD5, of a key and some data
 */
function core_hmac_md5(key, data)
{
  var bkey = str2binl(key);
  if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz);

  var ipad = Array(16), opad = Array(16);
  for(var i = 0; i < 16; i++)
  {
    ipad[i] = bkey[i] ^ 0x36363636;
    opad[i] = bkey[i] ^ 0x5C5C5C5C;
  }

  var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
  return core_md5(opad.concat(hash), 512 + 128);
}

/*
 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 * to work around bugs in some JS interpreters.
 */
function safe_add(x, y)
{
  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
  return (msw << 16) | (lsw & 0xFFFF);
}

/*
 * Bitwise rotate a 32-bit number to the left.
 */
function bit_rol(num, cnt)
{
  return (num << cnt) | (num >>> (32 - cnt));
}

/*
 * Convert a string to an array of little-endian words
 * If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
 */
function str2binl(str)
{
  var bin = Array();
  var mask = (1 << chrsz) - 1;
  for(var i = 0; i < str.length * chrsz; i += chrsz)
    bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
  return bin;
}

/*
 * Convert an array of little-endian words to a string
 */
function binl2str(bin)
{
  var str = "";
  var mask = (1 << chrsz) - 1;
  for(var i = 0; i < bin.length * 32; i += chrsz)
    str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
  return str;
}

/*
 * Convert an array of little-endian words to a hex string.
 */
function binl2hex(binarray)
{
  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i++)
  {
    str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
           hex_tab.charAt((binarray[i>>2] >> ((i%4)*8  )) & 0xF);
  }
  return str;
}

/*
 * Convert an array of little-endian words to a base-64 string
 */
function binl2b64(binarray)
{
  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i += 3)
  {
    var triplet = (((binarray[i   >> 2] >> 8 * ( i   %4)) & 0xFF) << 16)
                | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 )
                |  ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
    for(var j = 0; j < 4; j++)
    {
      if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
      else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
    }
  }
  return str;
}
