/**
 * @fileOverview
 * Nyx is not just the goddess of the night, she's also a mini-library of DAAPI reference gadgets.
 * 
 * This file includes scripts used throughout Nyx; (if needed), this defines 
 * <tt>window.console</tt> -- a safety wrapper for Firebug-type logging calls.
 * 
 * Everything else is namespaced into the object <tt>window.NYX</tt>.
 * 
 * @author Glen Ford (glenford [at] yahoo.com)
 * @namespace NYX
 */

/**
 * @class
 * Namespace object.
 */
var NYX = {
	version: "1.1"
}


/**
 * Defines a console object (with "empty" methods) as needed; allows code in any browser to
 * call Firebug console methods without error. Doesn't overwrite existing console so as to not
 * interfere with Safari's console. The Firebug methods array should be maintained in sync with
 * the actual Firebug console.
 */
NYX.fixConsole = function() {
	if (typeof window.console != "object") window.console = {};
	if (window.console.isNyxxed) {/*already fixed*/}
	else {
		var firebugMethods = ["log","debug","info","warn","error","assert","dir","dirxml",
			"trace","group","groupEnd","time","timeEnd","profile","profileEnd","count"];
		for (var methodIdx = 0; methodIdx < firebugMethods.length; methodIdx++) {
			var methodName = firebugMethods[methodIdx];
			if (typeof window.console[methodName] != "function") window.console[methodName] = function(){};
		}
	}
	//add our tracking flag
	window.console.isNyxxed = true;
}

//goof-proof calls to the console -- IMMEDIATE EXECUTION HERE
NYX.fixConsole();


//now let's report the version (totally optional)
console.info("Nyx library loaded, version " + NYX.version);


/** @class (Singleton) A hash for object instances to store "globals" without affecting the window namespace. */
NYX.cache = {}

/** 
 * Fixes the horrid IE Operation Aborted error pretty simply;
 * based on a trick by Diego Perini -- 
 * <a href="http://javascript.nwbox.com/IEContentLoaded/">http://javascript.nwbox.com/IEContentLoaded/</a>
 * <p>
 *   This is used like a constructor in order to handle parameters sent to your "action function".
 *   The first parameter sent must be your function; if you add additional parameters, these get
 *   passed through to your function.
 * </p>
 * 
 * @param {Function} the "action function" that does the unsafe operations 
 * @param {varargs}  optional: arguments to pass into the "action function"
 */
NYX.ieSafeExecution = function() {
	//store this in a closure, in case we do asynchronous operation
	var This = this;
	
	//a setting
	this.timeoutLength = 200;	//millis
	
	//the first argument must be the function to call; any remaining arguments get passed to that function
	if (typeof arguments[0] != "function") {
		throw("First parameter to NYX.ieSafeExecution is required and must be a function");
	} else {
		this.functionToCall = arguments[0];
	}
	
	this.execute = function() {
		//on the first call, we need to cache the arguments
		if (typeof This.arguments == "undefined") This.arguments = arguments;
		
		if ( typeof document.body.attachEvent == "object" && (document.readyState != "loaded" && document.readyState != "complete") ) {
			if (typeof console == "object") console.log("in the IE block");
			try {
				document.documentElement.doScroll("left");
				This.functionToCall.apply(This.functionToCall, This.arguments);
			} catch(error) {
				setTimeout(This.execute, This.timeoutLength);
			}
		} else {
			if (typeof console == "object") console.log("executing immediately");
			This.functionToCall.apply(This.functionToCall, This.arguments);
		}
	}
}


/** @class (Singleton) A bag of various utilities */
NYX.util = {}

/**
 * Returns a "probably unique identifier" (string) based on a random number and the timestamp.
 * It's not quite a GUID, but the chances of duplication in a real system are pretty
 * low.
 * @param {Object} [optExtraDigits] larger number results in more security
 * @param {Object} [optBase]		used in toString -- a higher number results in a smaller PUID
 */
NYX.util.makePuid = function(optExtraDigits, optBase) {
	var timeSeed, rnd, puid;
	if (typeof optExtraDigits != "number") optExtraDigits = 5;
	if (typeof optBase != "number") optBase = 32;
	timeSeed = ( new Date().valueOf() ) - Date.parse("1/1/2008");
	rnd = Math.random().toString().substr(2, optExtraDigits); //expects Math.random to return x.YYY...
	puid = parseInt(timeSeed + "" + rnd).toString(optBase);
	
	return puid;
}

/** @class (Singleton) Tools for dealing with querystrings */
NYX.util.querystring = {
	/**
	 * @return {String} the value of the specified querystring parameter;
	 * 					null if the variable isn't defined
	 * 
	 * @param {String} name				Name of the QS variable
	 */
	get: function(name) {
		var key = name + "=";
		var nameValuePairs = document.location.search.substring(1).split("&");
		for (var pairIdx = 0; pairIdx < nameValuePairs.length; pairIdx++) {
			if ( nameValuePairs[pairIdx].indexOf(key) == 0 ) {
				return nameValuePairs[pairIdx].substring(key.length);
			}
		}
		return null;
	}
	,
	/**
	 * Sets or adds the specified querystring parameter. Returns the querystring with a 
	 * leading "?" even if it didn't have one before.
	 * 
	 * <p><i>Notes:</i>
	 * 	If the parameter exists, it <b>must</b> have the trailing equals sign, even if the value is empty.
	 * 	Names are case sensitive.
	 * </p>
	 * 
	 * @return {String} <code>location.search</code> or the optional querystring argument
	 * 					with the given parameter set to the value passed.
	 * @param {String} name				Name of the QS variable
	 * @param {String} value			Value to set
	 * @param {String} [optExistingQS]	QS to use (defaults to location.search)
	 */
	set: function(name, value, optExistingQS) {
		//get the qs to work with 
		if (typeof optExistingQS != "string") {
			var qs = location.search;
		} else {
			var qs = optExistingQS;
		}
		var theReturn = qs;
		
		if (typeof value == "undefined") value = "";
		
		//escape the name-value pair
		var nvp = encodeURI(name + "=" + value);
		
		if (qs == "") {
			theReturn = nvp;
		} else if ( !NYX.util.string.contains(qs, name) ) {
			theReturn = qs + "&" + nvp;
		} else {
			//use a regex to replace the variable
			var regex = new RegExp("(" + name + "=[^&^]*)", "gi");
			
			//this test also loads the global RegExp object
			if ( qs.match(regex) != null ) {
				theReturn = qs.replace(RegExp.lastMatch, nvp);
			}
		}
		
		if ( !NYX.util.string.startsWith(theReturn, "?") ) theReturn = "?" + theReturn;
		return theReturn;
	}
}

/** @class (Singleton) Object-orientation tools */
NYX.util.obj = {
	/** 
	 * Mimics the Java <code>extends</code> keyword.
	 * Copies all members (properties and methods) from the parent into the target.
	 * <p>In addition to inheritance, it's useful for copying "parameter bags" (JSON objects) into an instance.</p>
	 * <p>To override members, change them after the <code>extendObj</code> call.</p>
	 * @param {Object} targetClass	The child class to modify.
	 * @param {Object} parentClass	The class to extend from.
	 */
	extend: function(targetClass, parentClass) {
		for (var member in parentClass) {
			targetClass[member] = parentClass[member];
		}
		return targetClass;
	}
}

/** @class (Singleton) Utilities for Array that the JS gods forgot to put in */
NYX.util.array = {
	/**
	 * Returns true if an item is a member of the array. <tt>optPropName</tt> is used
	 * to a property under theArray (for instance, if it's an array of objects,
	 * this will search against the specified property of each array member.) 
	 * <b>Example:</b>
	 * <pre>
	 * 	myArray = [ {x:1, y:1}, {x:1}, {x:1, y:2} ]
	 * 	test = NYX.util.array.contains(myArray, 2, "y")	//test is true
	 * 	test = NYX.util.array.contains(myArray, 2, "x")	//test is false
	 * 	test = NYX.util.array.contains(myArray, 1, "z")	//test is false
	 * </pre>
	 * 
	 * @return {boolean} true if item was found in the array
	 * @param {Object} theArray		 Array to look through
	 * @param {Object} match		 Item to look for
	 * @param {Object} [optPropName] If passed, array members are objects and this specifies
	 * 									the property in each member to look at
	 */
	contains: function(theArray, match, optPropName) {
		if (theArray.length) {
			var member;
			for (var idx = 0; idx < theArray.length; idx++) {
				if (typeof optPropName == "undefined") member = theArray[idx];
				else member = theArray[idx][optPropName];
				if (member == match) {
					return true;
				}
			}
		}
		return false;	//if we get here, there's no match
	}
}

/** @class (Singleton) Utilities for String that the JS gods forgot to put in */
NYX.util.string = {
	/**
	 * Checks source for presence of match. Source is converted to a string as needed,
	 * but that doesn't guarantee this will work for arrays. The caller must manipulate arrays as needed
	 * if using this function to check for an element.
	 * 
	 * @return {Boolean} true if the string contains match
	 * 
	 * @param {String} source	string to search through
	 * @param {String} match	string to search for
	 * @param {Boolean} [optIgnoreCase]	if true, case is ignored
	 */
	contains: function(source, match, optIgnoreCase) {
		if (optIgnoreCase) {
			source = source.toLowerCase();
			match  = match.toLowerCase();
		}
		return (source.indexOf(match) > -1);
	}
	,
	/**
	 * @return {String} this string with all occurrences of the substring replaced
	 * @param {String} source	string to search through
	 * @param {String} match		The sub-string to kill
	 * @param {String} replacement	
	 */
	replaceAll: function(source, match, replacement) {
		while ( this.contains(source, match) ) {
			source = source.replace(match, replacement);
		}
		return source;
	}
	,
	/**
	 * Converts the argument to a string even if it is null, etc.
	 * @return {String} A 0-length string for nulls/undefined argument; else the argument as a string.
	 */
	ensure: function(arg) {
		if (typeof arg == "string") return arg;
		if (arg == null || typeof arg == "undefined") return "";
		return arg.toString();
	}
	,
	/** @return {String} The string passed with leading and trailing whitespace removed. */
	trim: function(stringToTrim) {
		stringToTrim = this.ensure(stringToTrim);
		return stringToTrim.replace(/(^\s+|\s+$)/g, "");
	}
	,
	/** @return {Boolean} true if the string passed is 0-length or only whitespace */
	isBlank: function(source) {
		return (this.trim(source) == "");
	}
	,
	/** 
	 * @return {Boolean} true if the string starts with the fragment passed (case-sensitive)
	 * @param {String} source	string to search through
	 * @param {String} match	Fragment to look for
	 * @param {Boolean} [optIgnoreCase]	if true, case is ignored
	 */
	startsWith: function(source, match, optIgnoreCase) {
		return ( source.substring(0, match.length) == match );
	}
	,
	/** 
	 * @return {Boolean} true if the string ends with the fragment passed (case-sensitive)
	 * @param {String} source	string to search through
	 * @param {String} match	Fragment to look for
	 * @param {Boolean} [optIgnoreCase]	if true, case is ignored
	 */
	endsWith: function(source, match, optIgnoreCase) {
		if (optIgnoreCase) {
			source = source.toLowerCase();
			match  = match.toLowerCase();
		}
		return ( source.substring(source.length - match.length) == match );
	}
}

/** @class (Singleton) Help for JSON, especially as it is used in SiteLife DAAPI */
NYX.util.json = {
	/** @return {string} theString with single- and double-quotes replaced by entities */
	escape: function(theString) {
		theString = theString.replace('"', "&quot;");
		theString = theString.replace("'", "&#39;");
		return theString;
	}
	,
	/** 
	 * Reverses what escape does
	 * @return {string} theString with single- and double-quotes entities restored
	 */
	unescape: function(theString) {
		theString = theString.replace("&quot;", '"');
		theString = theString.replace("&#39;", "'");
		return theString;
	}
}

/** @class (Singleton) DOM-related utilities */
NYX.util.dom = {
	/**
	 * Returns an array of <b>direct</b> child elements that have the given 
	 * CSS class. (This is based on Prototype, but their way causes weird bugs in IE.)
	 * 
	 * <blockquote>TODO: needs to be improved -- add recursion.</blockquote>
	 * 
	 * @return {DOM:Element[]} Returns an empty array if nothing found.
	 * @param {String} 		className to match
	 * @param {DOM:Element}	[optElem]	if not passed, <code>document.body</code> is used
	 */
	getElementsByClass: function(className, optElem) {
		var theReturn = [];
		if (typeof optElem == "undefined") var elem = document.body;
		
		var children = elem.getElementsByTagName("*");
		for (var childIdx = 0; childIdx < children.length; childIdx++) {
			var child = children[childIdx];
			if ( typeof child.className == "string" && this.elemHasClass(child, className) ) {
				theReturn.push(child);
			}
		}
		return theReturn;
	},
	/**
	 * @return {Boolean} True if the DOM element has the CSS class applied to it.
	 * @param {DOM:Element}	elem		
	 * @param {String} 		className	
	 */
	elemHasClass: function(elem, className) {
		var currentClasses = elem.className.toLowerCase().split(/\s+/g);
		return NYX.util.array.contains( currentClasses, className.toLowerCase() );
	}
}



/** 
 * @class 
 * An "abstract superclass" for objects that use templates to write their GUI.
 * Properties here can be overridden in the implementing objects as needed.
 */
NYX.TemplateTool = { 
	//template part names -- internal use, but can be overridden
	DOM_TARGET_SUFFIX:  "dynamicContent",
	DOM_WAITMSG_SUFFIX: "waitMsg",
	
	//classes for item coloring (see below)
	/** CSS class applied to every other line item (when the GUI contains multiple items) @type {String} */
	ALT_CLASS_2: "nyx2",
	
	/** CSS class applied to every other 3rd item (when the GUI contains multiple items)  @type {String} */
	ALT_CLASS_3: "nyx3",
	
	getElem: function(idSuffix) {
		return document.getElementById(this.idRoot + "_" + idSuffix);
	}
	,
	getIndexedElem: function(idSuffix, index) {
		return document.getElementById(this.idRoot + "_" + idSuffix + "_" + index);
	}
	,
	showElem: function(idSuffix) {
		var elem = this.getElem(idSuffix);
		if (elem != null) elem.style.display = "";
	}
	,
	/**
	 * "Flattens" a discovered content item (or any object) by copying grandchild members (variables within sub-objects)
	 * up a level, so that
	 * <tt>daapiItem.Comments.NumberOfComments</tt>
	 * becomes
	 * <tt>daapiItem.NumberOfComments</tt>
	 * and so on.
	 * 
	 * @param {Object} daapiItem	a DiscoveredContent array member, or any object
	 */
	flattenDaapiItem: function(daapiItem) {
		var flatData = {};
		for (var child in daapiItem) {
			if (daapiItem[child] == null) {
				flatData[child] = null;
			} else if (typeof daapiItem[child] != "object") {
				//scalar value
				flatData[child] = daapiItem[child];
			} else {
				//non-scalar, so recurse
				if (typeof daapiItem[child].join == "function" && typeof daapiItem[child].length == "number") {
					//we're going to assume it's an array and just assign it
					flatData[child] = daapiItem[child];
				} else {
					//we have an object, so recurse one level and set members that haven't already been set
					for (var grandchild in daapiItem[child]) {
						if (typeof flatData[grandchild] == "undefined") flatData[grandchild] = daapiItem[child][grandchild];
					}
				}
			}
		}
		return flatData;
	}
	,
	processTemplate: function(dataObj, template) {
		//finds template variables in the template passed and replaces them *if* the dataObj has a matching defined member
		
		var regex, template, matches, matchIdx, theMatch, varName;
		regex = /\@Nyx\.[^\@]+\@/g;
		
		var matches = template.match(regex);
		if (matches != null) {
			for (matchIdx = 0; matchIdx < matches.length; matchIdx++) {
				theMatch = matches[matchIdx];
				
				//varName = theMatch.substring( theMatch.indexOf(".") + 1 , theMatch.lastIndexOf("@") );
				varName = theMatch.substring(5, theMatch.length - 1);
				
				if (typeof dataObj[varName] != "undefined") {
					template = NYX.util.string.replaceAll(template, theMatch, dataObj[varName]);
				}
			}
		}
		
		return template;
	}
	,
	applyAltClasses: function(itemIndex, template) {
		/* edits the HTML passed in template to apply alternate-row class names on an every-other and every-third basis
		 * 
		 * IMPORTANT -- itemIndex is expected to be 0-based
		 */
		//template must contain Nyx.AlternateClass variable
		var className, templateVariable;
		templateVariable = "@Nyx.AlternateClass@";
		if (template.indexOf(templateVariable) > -1) {
			className = " ";
			if ( (itemIndex + 1) % 2 == 0 ) {
				//every-other style
				className += this.ALT_CLASS_2 + " ";
			}
			if ( (itemIndex + 1) % 3 == 0 ) {
				//every-third style
				className += this.ALT_CLASS_3 + " ";
			}
			template = NYX.util.string.replaceAll(template, templateVariable, className);
		}
		return template;
	}
	,
	showTarget: function(domTarget) {
		//hide the wait message
		var waitingMsgElem = this.getElem(this.DOM_WAITMSG_SUFFIX);
		if (waitingMsgElem != null) {
			waitingMsgElem.style.display = "none";
		}
		
		//show the target element
		if (domTarget != null) {
			domTarget.style.display = "";
		}
	}
}
