/**
 * extensions.js: Essential functions and extensions to native JS classes.
 *
 * @package extensions
 * @author Erik Fleischer
 * @version 2.1 (02/07)
 */


/**
 * Counts and returns the number of non-inherited properties of the object 
 * (which, in the case of an array, includes all its numbered elements).
 *
 * @param void
 * @return integer
 */
Object.prototype.count = function() {
	var _count = 0;
	for (var _prop in this)
	{
		// Only count non-inherited properties.
		if ( this.hasOwnProperty(_prop) )
		{
			_count++;
		}
	}
	return _count;
}

/**
 * Searches object or array for first occurrence of value and, if found, returns the corresponding property name or index.
 *
 * @param mixed _value Value of object property or array element whose property name, or array index, is being looked for.
 * @param boolean _strict Whether or not value should be identical (not just equal), to the supplied parameter. [optional]
 * @return mixed
 */
Object.prototype.getIndex = function(_value, _strict) {
	if (typeof _value == "undefined")
	{
		throw new Error("Missing first argument to Object.prototype.getIndex()");
	}
	for (var _prop in this)
	{
		// Ignore inherited properties.
		if ( this.hasOwnProperty(_prop) )
		{
			if ( (_strict && this[_prop] === _value) || (!_strict && this[_prop] == _value) )
			{
				return _prop;
			}
		}
	}
	return false;
}

/**
 * Performs a shallow search (i.e. one level only) and returns true if needle 
 * (_value) is contained in haystack (object itself), false otherwise.
 *
 * @param mixed _value Value of object property or array element whose presence is being looked for.
 * @param boolean _strict Whether or not value should be identical, rather than just equal, to the supplied parameter. [optional]
 * @return boolean
 */
Object.prototype.hasValue = function(_value, _strict) {
	if (typeof _value == "undefined")
	{
		throw new Error("Missing first argument to Object.prototype.hasValue()");
	}
	_strict = (typeof _strict != "undefined" && _strict) ? true : false;

	if (typeof this.hasOwnProperty == "undefined")
	{
		throw new Error("Object.prototype.hasValue() failed: browser does not implement Object.prototype.hasOwnProperty()");
	}

	for (var _prop in this)
	{
		// Ignore inherited properties.
		if ( this.hasOwnProperty(_prop) )
		{
			if ( (_strict && this[_prop] === _value) || (!_strict && this[_prop] == _value) )
			{
				return true;
			}
		}
	}

	return false;
}

/**
 * Returns true if the string representation of the object is numeric, false otherwise.
 *
 * @param void
 * @return boolean
 */
Object.prototype.isNumeric = function() {
	if ( this.toString().match(/^\d+$/) )
	{
		return true;
	}
	return false;
}

/**
 * Shuffles array elements in place, i.e. performs random re-arrangement of 
 * sequence of array elements.
 *
 * @param void
 * @return void
 */
Array.prototype.shuffle = function() {
	this.sort( function(a, b) { return Math.random() - Math.random(); } );
}

/**
 * Attempts to replace all occurrences of the elements in _arrayFrom with their counterparts in _arrayTo.
 *
 * @param array _arrayFrom Array of strings that should be replaced.
 * @param array _arrayTo Array of string replacements to be substituted for "from" strings.
 * @return string
 */
String.prototype.arrayReplace = function(_arrayFrom, _arrayTo) {
	if (!_arrayFrom || !_arrayFrom.constructor || _arrayFrom.constructor != Array)
	{
		throw new Error("Missing or invalid first argument passed to String.arrayReplace(): must be an array");
	}
	if (!_arrayTo || !_arrayTo.constructor || _arrayTo.constructor != Array)
	{
		throw new Error("Missing or invalid second argument passed to String.arrayReplace(): must be an array");
	}
	if (_arrayFrom.length != _arrayTo.length)
	{
		throw new Error("String.arrayReplace() failed: search and replace arrays have different lengths");
	}

	var _output = this.toString();
	for (var _i = 0; _i < _arrayFrom.length; _i++)
	{
		try
		{
			var _regexp = new RegExp(_arrayFrom[_i], "g");
		}
		catch(_error)
		{
			continue;
		}
		_output = _output.replace(_regexp, _arrayTo[_i]);
	}

	return _output;
}


/**
 * Returns base string padded to desired length with desired padding string. Pad type can be LEFT, RIGHT or BOTH; 
 * default is LEFT. If length is equal to or shorter than the length of the base string, or if no padding string
 * is specified, the original string is returned unchanged.
 * 
 *
 * @param integer _length Array of strings that should be replaced.
 * @param string _padding _arrayTo Array of string replacements to be substituted for "from" strings.
 * @param string _type Specifies whether padding is to be appended to the beginning (LEFT), end (RIGHT) or 
 * both (BOTH) of base string. Default is LEFT. [optional]
 * @return string
 */
String.prototype.pad = function(_length, _padding, _type) {
	if (!_length)
	{
		return this.toString();
	}
	else if (typeof _length != "number")
	{
		_length = Number(_length);
	}
	_length = _length - this.length;
	if (_length <= 0)
	{
		return this.toString();
	}

	if (!_padding)
	{
		return this.toString();
	}
	else if (typeof _padding != "string")
	{
		_padding = String(_padding);
	}

	if ( !_type || (_type.toUpperCase() != "LEFT" && _type.toUpperCase() != "RIGHT" && _type.toUpperCase() != "BOTH") )
	{
		var _type = "LEFT";
	}

	if (_type.toUpperCase() == "BOTH")
	{
		var _appendage = new Array();
		_appendage[0] = new String();	// Left appendage.
		_appendage[1] = new String();	// Right appendage.
		var _side = 0;	// To alternate between left and right appendages.
		for (var _i = 0; _i < _length; _i++)
		{
			_side = _i % 2;
			_appendage[_side] += _padding;
		}
	}
	else
	{
		var _appendage = new String();
		for (var _i = 0; _i < _length; _i++)
		{
			_appendage += _padding;
		}
	}

	var _output = new String();
	if (_type.toUpperCase() == "LEFT")
	{
		_output = _appendage + this.toString();
	}
	else if (_type.toUpperCase() == "RIGHT")
	{
		_output = this.toString() + _appendage;
	}
	else
	{
		_output = _appendage[0] + this.toString() + _appendage[1];
	}

	return _output;
}


/**
 * Returns base string with any whitespace characters at the beginning or at the end removed.
 *
 * @param void
 * @return string
 */
String.prototype.trim = function() {
	return this.replace(/^[\0\t\n\v\f\r\u0020\u00A0]+|[\0\t\n\v\f\r\u0020\u00A0]+$/g, "");
}


/**
 * Returns date as an integer in yyyymmdd format.
 *
 * @param void
 * @return integer
 */
Date.prototype.getDateYMD = function () {
	var _year = String((new Date).getFullYear());
	var _month = String((new Date).getMonth() + 1).pad(2, "0");
	var _day = String((new Date).getDate()).pad(2, "0");
	return Number(_year + _month + _day);
}


/**
 * Returns date as a string in yyyy-mm-dd format (separator can be any desired string).
 *
 * @param string _separator String to be used between year and month, and month and day. Defaults to hiphen. [optional]
 * @return string
 */
Date.prototype.getDateYMDString = function (_separator) {
	_separator = (typeof _separator == "string") ? _separator : "-";
	var _year = String((new Date).getFullYear());
	var _month = String((new Date).getMonth() + 1).pad(2, "0");
	var _day = String((new Date).getDate()).pad(2, "0");
	return(_year + _separator + _month + _separator + _day);
}


/**
 * Returns true if variable is defined, not null, and not identical to false; false otherwise.
 *
 * @param mixed _var Variable or value whose existence is to be checked.
 * @return boolean
 */
function isset (_var)
{
	if (typeof _var != "undefined" && _var !== null && _var !== false)
	{
		return true;
	}
	return false;
}


/**
 * Finds and returns the node's (first parameter) ancestor that has the specified node name (second parameter).
 * If the third parameter is present and true, the search will continue until the uppermost ancestor is found.
 * If no ancestor with the desired node name is found, null is returned.
 *
 * @param mixed _node Element object (or its ID string) whose ancestor node is being looked for.
 * @param string _ancestorNodeName Element name of desired ancestor node.
 * @param string _uppermost Specifies whether search should continue until the uppermost ancestor is found. [optional]
 * @return object
 */
function getAncestor (_node, _ancestorNodeName, _uppermost)
{
	_node = getElement(_node);
	if (typeof _node.parentNode == "undefined")
	{
		throw new Error("Invalid first argument passed to getAncestor(): element does not have a parent node");
	}
	if (typeof _ancestorNodeName != "string")
	{
		throw new Error("Missing or invalid second argument passed to getAncestor(): not a string");
	}

	var _currentNode = _node;
	var _acestor = null;
	do
	{
		_currentNode = _currentNode.parentNode;
		if (_currentNode && _currentNode.nodeName.toLowerCase() == _ancestorNodeName.toLowerCase())
		{
			_acestor = _currentNode;
			if (!_uppermost)
			{
				break;
			}
		}
	} while (_currentNode);

	return _acestor;
}


/**
 * Attempts to find and return the value of an element's CSS property. If no such property is found, 
 * an empty string is returned.
 *
 * @param mixed _elem Element object (or its ID string) whose CSS property value is being looked for.
 * @param string _prop Name of CSS property whose value is being looked for.
 * @return string
 */
function getCSSProperty (_elem, _prop)
{
   	_elem = getElement(_elem);

	if (typeof window.getComputedStyle != "undefined")
	{
		var _computedStyle = window.getComputedStyle(_elem, "");
		return _computedStyle.getPropertyValue(_prop);
    }
    else if (typeof _elem.currentStyle != "undefined")
	{
        return _elem.currentStyle[_prop];
    }
    return "";
}


/**
 * Attempts to find and return a document element object of the required type (if any is specified). If an 
 * element object is passed as the first argument and it is the right type, it will be returned unchanged;
 * if a string is passed as the first argument, an element (if any) havind that string as an ID will be returned.
 *
 * @param mixed _elem Element object (or its ID string) being looked for.
 * @param mixed _reqType String representing node name or integer representing node type of desired element. [optional]
 * @return object
 */
function getElement (_elem, _reqType)
{
	if ( typeof _elem == "string" && !( _elem = document.getElementById(_elem) ) )
	{
		throw new Error("Invalid first argument (\"" + _elem + "\") passed to getElement(): not a valid element ID");
	}
	else if (typeof _elem != "object" || typeof _elem.nodeType == "undefined" || typeof _elem.nodeName == "undefined")
	{
		throw new Error("Missing or invalid first argument to getElement(): not an element object");
	}

	if ((typeof _reqType == "string" && _elem.nodeName.toLowerCase() == _reqType.toLowerCase()) || 
		(typeof _reqType == "number" && _elem.nodeType == _reqType) || 
		[1, 9].hasValue(_elem.nodeType))
	{
		return _elem;
	}
	else
	{
		throw new Error("Invalid first argument passed to getElement(): object of type/name " + 
						_reqType.toString() + "expected");
	}
}


/**
 * Returns all the descendant element nodes of a given base node that have a certain attribute with a certain value 
 * (if specified) and whose node names are among the required ones (if specified).
 *
 * @param mixed _baseNode Element object (or its ID string) whose descendant element nodes are being searched for matches.
 * @param string _attribName Name of attribute that desired nodes must contain.
 * @param string _attribValue Value of attribute that desired nodes must contain. [optional]
 * @param mixed _reqNodes Object or array having as propertie values or elements all the acceptable node names desired.
 * @return array
 */
function getElementsByAttribute (_baseNode, _attribName, _attribValue, _reqNodes)
{
	try
	{
		_baseNode = getElement(_baseNode);
	}
	catch(e)
	{
		throw new Error("Invalid first argument passed to getElementsByAttribute(): element object or element ID expected");
	}
	if (typeof _attribName != "string")
	{
		throw new Error("Invalid second argument passed to getElementsByAttribute(): string expected");
	}
	_attribValue = isset(_attribValue) ? _attribValue : null;
	if (_attribValue && typeof _attribName != "string")
	{
		throw new Error("Invalid third argument passed to getElementsByAttribute(): string or null expected");
	}
	_reqNodes = isset(_reqNodes) ? ((typeof _reqNodes == "object") ? _reqNodes : [_reqNodes]) : null;

	var _output = new Array();
	var _descendants = _baseNode.getElementsByTagName("*");
	for (var _i = 0; _i < _descendants.length; _i++)
	{
		var _currentAttribute = _descendants[_i].getAttribute(_attribName);
		if ( _currentAttribute && (!_attribValue || _currentAttribute == _attribValue) && 
			(!_reqNodes || _reqNodes.hasValue(_descendants[_i].nodeName.toLowerCase())) )
		{
			_output.push(_descendants[_i]);
		}
	}
	return _output;
}


/**
 * Returns all the explicit attributes of an element, i.e., only attributes explicitly specified in document source.
 *
 * @param mixed _elem Element object (or its ID string) whose attributes are being looked for.
 * @return array
 */
function getExplicitAttributes (_elem)
{
	_elem = getElement(_elem);

	var _output = new Array();

	// Extract "attributes" named node map from bogus element for verification purposes.
	var _controlAttribs = document.createElement("").attributes;
	var _controlAttribNames = new Array();
	for (var _i = 0; _i < _controlAttribs.length; _i++)
	{
		_controlAttribNames.push( _controlAttribs.item(_i).nodeName );
	}

	for (var _i = 0; _i < _elem.attributes.length; _i++)
	{
		var _currentAttribute = _elem.attributes.item(_i);
		if ( !_controlAttribNames.hasValue(_currentAttribute.nodeName) )
		{
			_output.push(_currentAttribute);
		}
	}

	return _output;
}


/**
 * Returns the mouse coordinates when the desired event is captured. Coordinates are returned as an object having 
 * properties "x" (for the "left" coordinate) and "y" (for the "top" coordinate).
 *
 * @param object _event Event object (in browsers that implement standard DOM event model) or null in IE.
 * @return boolean
 */
function getMousePosition (_event)
{
	_event = (typeof _event != "undefined") ? _event : ((typeof window.event != "undefined") ? window.event : null);
	if (!_event)
	{
		throw new Error("Missing or invalid first argument passed to getMousePosition(): must be a mouse event object");
	}

	var _coordinates = {"x":0, "y":0};
	if (typeof _event.pageX != "undefined" && typeof _event.pageY != "undefined")
	{
		_coordinates.x = _event.pageX;
		_coordinates.y = _event.pageY;
	}
	else if (typeof _event.clientX != "undefined" && typeof document.body != "undefined" && 
			 typeof document.body.scrollLeft != "undefined" && typeof document.body.clientLeft != "undefined")
	{
		_coordinates.x = _event.clientX + document.body.scrollLeft - document.body.clientLeft;
		_coordinates.y = _event.clientY + document.body.scrollTop - document.body.clientTop;
	}
	return _coordinates;
}


/**
 * Attempts to evaluate a JavaScript code string in the global scope. Failing that, attempts to create a script 
 * element and append it to the document's head element.
 *
 * @param string _jsString JavaScript string to be evaluated.
 * @return boolean
 */
function globalEval (_jsString)
{
	if (typeof _jsString != "string")
	{
		return false;
	}

	// Check whether window.eval executes code in the global scope.
	window.eval("var __INCLUDE_TEST_1__ = true;");
	if (typeof window.__INCLUDE_TEST_1__ != "undefined")
	{
		delete window.__INCLUDE_TEST_1__;
		window.eval(_jsString);
	}
	else
	{
		delete __INCLUDE_TEST_1__;
		if (typeof window.execScript != "undefined")	// IE only
		{
			window.execScript(_jsString);
		}
		else
		{
			// Last-ditch attempt (which seems to be the only thing that works in Safari): convert all function 
			// declarations to properties of the window object and eval the code.
			_jsString = _jsString.replace(/function\s+(\w+)\s+/g, "window.$1 = function");
			eval(_jsString);
		}
	}
	
	return true;
}


/**
 * Creates and returns instance of HTTPRequest class, a cross-platform XMLHttpRequest wrapper.
 *
 * @param void
 * @return object
 */
function HTTPRequest ()
{
	// Determine whether standard DOM Level 2 is supported.
	if (typeof window.XMLHttpRequest != "undefined")
	{
		this.$request = new XMLHttpRequest();
	}
	// If not, use Microsoft's proprietary API.
	else if (typeof window.ActiveXObject != "undefined")
	{
		var _apiVersions = ["MSXML2.XmlHttp.6.0", "MSXML2.XmlHttp.5.0", "MSXML2.XmlHttp.4.0", 
							"MSXML2.XmlHttp.3.0", "MSXML2.XmlHttp", "Microsoft.XmlHttp"];
		for (_i = 0; _i < _apiVersions.length; _i++)
		{
			try
			{
				this.$request = new ActiveXObject(_apiVersions[_i]);
			}
			catch(_error)
			{
				// Do nothing.
			}
		}
	}
	else
	{
		throw new Error("HTTPRequest() failed: neither the standard DOM Level 2 " + 
						"nor the proprietary Microsoft API is supported");
	}
}

/**
 * Makes an XMLHttpRequest call (synchronous or asynchronous) to a URL using the desired method and returns
 * an object containing the request's plain text and XML responses, if any (which will be empty if the call is
 * asynchronous).
 *
 * @param string _url URL to which call is to be made.
 * @param mixed _data Data, if any, to be sent to URL.
 * @param string _method Method to be used for making call.
 * @param object _headers HTTP header names and values (as object property names + respective values) to be issued. [optional]
 * @param function _responseHandler Function to handle response on asynchronous calls. [optional]
 * @param mixed _responseHandlerArgs Arguments to be passed to response handler function on asynchronous calls. [optional]
 * @return object
 */
HTTPRequest.prototype.send = function(_url, _data, _method, _headers, _responseHandler, _responseHandlerArgs) {
	if (!_url || typeof _url != "string")
	{
		throw new Error("Missing or invalid first argument passed to HTTPRequest.send(): URL string expected");
	}
	if (!_data)
	{
		var _data = null;
	}
	if ( typeof _method != "string" || !["POST", "GET"].hasValue((_method = _method.toUpperCase())) )
	{
		var _method = "POST";
	}
	if (_headers && typeof _headers != "object")
	{
		throw new Error("Invalid fifth argument passed to HTTPRequest.send(): array expected");
	}
	if (_responseHandler)
	{
		if (typeof _responseHandler != "function")
		{
			throw new Error("Invalid second argument passed to HTTPRequest.send(): function expected");
		}

		if (!_responseHandlerArgs)
		{
			var _responseHandlerArgs = new Array();
		}
		else if (!_responseHandlerArgs.constructor || _responseHandlerArgs.constructor != Array)
		{
			_responseHandlerArgs = new Array(_responseHandlerArgs);
		}
	}

	if (_responseHandler)
	{
		var _requestRef = this.$request;
		this.$request.onreadystatechange = function() {
			if (_requestRef.readyState == 4)
			{
				var _response = false;
				if (_requestRef.status == 200)
				{
					if (_requestRef.responseXML && _requestRef.responseXML.documentElement)
					{
						_response = _requestRef.responseXML;
					}
					else if (_requestRef.responseText)
					{
						_response = _requestRef.responseText;
					}
				}
				_responseHandler.apply(_response, _responseHandlerArgs);
			}
		}
	}
	
	var _dataString = this.$data;	// Create reference to data property so it can be used as a function argument.
	var _asynchronous = _responseHandler ? true : false;
	this.$request.open(_method, _url, _asynchronous);
	if (_headers)
	{
		for (var _prop in _headers)
		{
			if (_headers.hasOwnProperty(_prop))
			{
				this.$request.setRequestHeader(_prop, _headers[_prop]);
			}
		}
	}
	this.$request.send(_data);
	if (!_asynchronous)
	{
		if (this.$request.status == 200)
		{
			return {"text":this.$request.responseText, "xml":this.$request.responseXML};
		}
		else
		{
			return false;
		}
	}
	return {"text":"", "xml":""};
}


/**
 * Retrives JavaScript file via synchronous XMLHttpRequest call and evaluates its contents in the global scope or, 
 * failing that, creates a new script element with the code to be included and appends it to the document's head.
 *
 * @param string _filePath Web-accessible path of JavaScript file to be retrieved and "included".
 * @return boolean
 */
function include (_filePath)
{
	if (!_filePath || typeof _filePath != "string")
	{
		throw new Error("Missing or invalid argument passed to include(): string expected");
	}
	if (typeof document.CACHE != "object")
	{
		document.CACHE = new Object();
	}
	if (typeof document.CACHE.loadedScripts != "object")
	{
		document.CACHE.loadedScripts = new Object();
	}
	if (typeof document.CACHE.loadedScripts[_filePath] != "undefined")
	{
		return true;
	}

	var _httpRequest = new HTTPRequest();
	var _response = _httpRequest.send(_filePath, null, "GET");

	if (!_response || typeof _response.text == "undefined" || !_response.text.length)
	{
		return false;
	}

	globalEval(_response.text);

	document.CACHE.loadedScripts[_filePath] = true;
	return true;
}


/**
 * Adds event listener (function) to document element and passes optional arguments to event listener. Event itself
 * is always passed as argument to event listener as the last argument.
 *
 * @param mixed _target Document element object (or its ID string) to which event listener should be added.
 * @param string _event Name of event to be listened for.
 * @param function _handlerFunc Function that new event listener should be set to.
 * @param mixed _handlerArgs Arguments to be passed to event listener function, in addition to event itself. [optional]
 * @return boolean
 */
function setEventHandler (_target, _event, _handlerFunc, _handlerArgs)
{
	if (typeof _target == "string")
	{
		_target = getElement(_target);
	}
	else if (typeof _target != "object")
	{
		throw new Error("Missing or invalid first argument passed to setEventHandler(): target object expected");
	}

	if (!_event || typeof _event != "string")
	{
		throw new Error("Missing or invalid second argument passed to setEventHandler(): event string expected");
	}
	if (_event.substring(0, 2) == "on")
	{
		_event = _event.substring(2);
	}
	_event = _event.toLowerCase();

	if (!_handlerFunc || typeof _handlerFunc != "function")
	{
		throw new Error("Missing or invalid third argument passed to setEventHandler(): function expected");
	}

	if (typeof _handlerArgs == "undefined")
	{
		var _handlerArgs = [];
	}
	else if (typeof _handlerArgs.constructor == "undefined" || _handlerArgs.constructor != Array)
	{
		_handlerArgs = new Array(_handlerArgs);
	}

	var _handler = function(_eventObject) { _handlerFunc.apply(_target, _handlerArgs.concat(_eventObject)); }

	try {
		// Try DOM
		if (typeof _target.addEventListener != "undefined")
		{
			_target.addEventListener(_event, _handler, false);
		}
		// If DOM method is not available, try proprietary Microsoft method
		else if (typeof _target.attachEvent != "undefined")
		{
			_target.attachEvent("on" + _event, _handler);
		}
		// If all else fails, check whether
		else if (typeof document.getElementById != "undefined" && typeof _target["on" + _event] != "undefined")
		{
			if (typeof _target["on" + _event] == "function")
			{
				var _oldHandler = _target["on" + _event];
				_target["on" + _event] = function(_eventObject) { _oldHandler(_eventObject); _handler(_eventObject); }
			}
			else
			{
				_target["on" + _event] = _handler;
			}
		}
		return true;
	} catch (_e) {
		return false;
	}
}
/******************************************************************************/