/**
 * baseJS is a work-in-progress lightweight JavaScript library.
 * There is currently NO SUPPORT for Internet Explorer.
 * The original intention of this framework is to provide helper methods for iPhone 
 * development for all versions of the iPhone and iPod Touch Safari Browser.
 * Everything also is tested and works in Firefox 2+.
 *
 * Many of the methods and ideals in this document have been taken from Prototype, jQuery, and YUI.
 * Licensed under the Creative Commons Attribution-ShareAlike 3.0 United States.
 *
 * Written by Paul Armstrong
 * Contact: paul@paularmstrongdesigns.com
 * Site: http://paularmstrongdesigns.com/projects/basejs
 *
 * Internal methods and properties start with an underscore. You probably shouldn't use them.
 *
 * Build Date 2008-11-09 @ 21:02:56
 */
 
var userAgent = navigator.userAgent.toLowerCase();
var base = {
	/**
	* Add properties to an object
	* @param arguments		Last argument is the object of properties or methods 
	*							to apply to the all preceding parameters.
	*/
	extend: function() {
		function ext(destination, source) {
			for(var property in source) {
				destination[property] = source[property];
			}
		}
		if(arguments.length == 2) {
			ext(arguments[0], arguments[1])
		} else {
			var l = arguments.length, src = arguments[l-1], i = l-1;
			while(i--) { ext(arguments[i], src); }
		}
	},
	/**
	* Convenience browser checking. Thanks to prototype && jquery.
	* I will hopefully never really need this.
	*/
	browser: {
		version: (userAgent.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [])[1],
		webkit: /webkit/.test(userAgent),
		opera: /opera/.test(userAgent), // untested
		msie: /msie/.test(userAgent) && !/opera/.test(userAgent), // watch out for IE -- no support with baseJS
		mozilla: /mozilla/.test(userAgent) && !/(compatible|webkit)/.test(userAgent),
		msafari: /apple.*mobile.*safari/.test(userAgent)
	},
	/**
	* Check if the object is an array instance.
	* @param object	{object}	Object to check.
	*/
	isArray: function(object) {
		return Object.prototype.toString.call(object) === '[object Array]';
	},
	/**
	* Generate a URL-safe query string from the object.
	* @param object	{object}	Object to transcribe into a query string.
	*/
	toQueryString: function(object) {
		var params = [];
		for(var key in object) {
			var str = encodeURIComponent(key)+'=';
			var value = (base.isArray(object[key])) ? object[key].join(',') : object[key];
			str += encodeURIComponent(value);
			params.push(str)
		}
		return params.join('&');
	},
	/**
	* Create and fire custom events
	* @param eventName	{string}		Name of the event
	* @param memo			{object}		Memo parameters for the event (optional)
	*/
	_fire: function(element, eventName, memo) {
		var event = document.createEvent('HTMLEvents');
		event.initEvent(eventName, true, true);
		event.memo = memo || {};
		element.dispatchEvent(event);
	},
	// Whether or not the Query Selectors API are available set via script
	selectors: false,
	// Location of sizzle.js relative to your requested document
	sizzleSrc: '../sizzle/sizzle.js'
};

base.extend(Array.prototype, NodeList.prototype, {
	/**
	* Run a function on each item in the array
	* @param iterator		{function}		Function to run on each object key
	* @param context		{object}		Scope override (optional)
	*/
	each: function(iterator, context) {
		iterator = (context) ? iterator.bind(context) : iterator;
		try {
			var c = this.length, i = 0;
			while(i<c) { 
				// make sure it's not a function!
				if(typeof this[i] !== 'function') { iterator(this[i]);}
				i++;
			}
		} catch(e) { throw e; }
		return this;
	},
	/**
	* Run a function on each item in the array after a specified interval
	* @param iterator		{function}		Function to run on each object key
	* @param interval		{Number}		Number of milliseconds before each iteration is run (default 1000)
	* @param dir			{Number}		-1, 1 or null. Negative gives reverse iteration. (optional)
	* @param context		{object}		Scope override (optional)
	* @param callback		{function}		Function to fire after each item has been iterated. (optional)
	*/
	eachAfter: function(iterator, interval, dir, context, callback) {
		iterator = (context) ? iterator.bind(context) : iterator;
		callback = callback || function() {};
		var dir = dir || 1;
		var c = this.length, i = 0;
		if(dir < 0) {
			var t = c;
			c = i;
			i = (t == 0) ? 0 : t-1;
		}
		try {
			var eachIterator = setInterval(function() {
				if(i === c) { 
					if(callback) { callback(); }
					clearInterval(eachIterator);
					return this;
				} else {
					// make sure it's not a function!
					if(typeof this[i] !== 'function') { iterator(this[i]); }
					i = i+dir;
				}
			}.bind(this), (interval || 1000));
		} catch(e) { throw e; }
	}
});

base.extend(Function.prototype, {
	/**
	* Override scope of a function.
	* @param oScope	{object}		Object to override the scope of the function.
	* @param arguments	{*}			Any additional arguments to pass.
	*/
	bind: function() {
		var method = this, args = Array.prototype.slice.call(arguments), object = args.shift();
		return function() {
			return method.apply(object, args.concat(Array.prototype.slice.call(arguments)));
		}
	},
	/**
	* Override scope of a callback function on an event.
	* @param oScope	{object} Object to override the scope of the function.
	* @param arguments {*} Any additional arguments to pass.
	*/
	bindAsEventListener: function() {
		var method = this, args = Array.prototype.slice.call(arguments), object = args.shift();
		return function(event) {
			return method.apply(object, [event || window.event].concat(Array.prototype.slice.call(arguments)));
		}
	}
});

/**
 * io makes a new io object request
 * @param url			{string}	location to access
 * @param options		{object}	(optional)
 * @param method		{string}	post or get form method
 * @param asynchronous		{boolean}	Only true supported at this time.
 * @param contentType		{string}	Content mime type to send.
 * @param encoding		{string}	Content encoding.
 * @param params		{object}	Object of header parameters to send with the request.
 * @param format		{string}	Response type assumption: 'text', 'json', 'object', 'xml'
 * @param sanitizeJSON		{boolean}	Whether the JSON needs to be sanitized.
 * @param onUninitialized	{function}	Ready state callback.
 * @param onConnected		{function}	Ready state callback.
 * @param onRequested		{function}	Ready state callback.
 * @param onProcessing		{function}	Ready state callback.
 * @param onComplete		{function}	Ready state callback. Includes response object.
 * @param onFailure		{function}	Ready state callback. Includes response object. (recommended)
 * @param onSuccess		{function}	Ready state callback. Includes response object. (recommended)
 */
var io = function(url, options) {
	this.options = {
		method: 'post', 
		asynchronous: true,
		contentType: 'application/x-www-form-urlencoded',
		encoding: 'UTF-8',
		params: {},
		format: 'text',
		sanitizeJSON: false
	};
	
	base.extend(this.options, options || {});
	this.options.method = this.options.method.toLowerCase();
	
	this.xhr = new XMLHttpRequest();
	
	// convert the params
	var params = base.toQueryString(this.options.params);
	if(this.options.method.toLowerCase() == 'get') {
		url += (url.indexOf('?') >= 0) ? '&' : '?' + params;
	}
	
	try {
		this.xhr.open(this.options.method, url, this.options.asynchronous);

		this.xhr.onreadystatechange = this._onStateChange.bind(this);
		// set the request headers
		var headers = {
			'X-Requested-With': 'XMLHttpRequest',
			'Accept': '*/*'
		};
		if(this.options.method == 'post') {
			headers['Content-type'] = this.options.contentType+(this.options.encoding ? '; charset='+this.options.encoding : '');
		}
		for(var name in headers) {
			this.xhr.setRequestHeader(name, headers[name]);
		}
		
		this._body = this.options.method.toLowerCase() == 'post' ? (this.options.postBody || params) : null;
		this.xhr.send(this._body);
	} catch(e) {
		console.error('request error', e);
	}
};
base.extend(io, {
	/**
	* io.response filters through an io response object to give most concise information possible.
	* @param response		{object}		The XMLHttpRequest object.
	* @param format		{string}		Type to respond with: 'text', 'json', 'object', 'xml'
	* @param sanitize		{boolean}		Whether the JSON needs to be sanitized.
	*/
	response: function(response, format, sanitize) {
		switch(format.toLowerCase()) {
			case 'xml':
				var xml = (response.responseXML) ? response.responseXML : new String('');
				return xml;
			break;
			case 'json':
				var json = response.responseText;
				if(sanitize) { json = json.sub(/^\/\*-secure-([\s\S]*)\*\/\s*$/, '#{1}'); }
				
				try {
					var str = response.responseText.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
					if((/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str)) {
						return eval('('+json+')');
					}
				} catch(e) {
					console.error('json eval error', e);
				}
			break;
			case 'text':
				return response.responseText;
			break;
			default:
				return response;
			break;
		}
	}
});

base.extend(io, {
	Events: ['Uninitialized', 'Connected', 'Requested', 'Processing', 'Complete', 'Failure', 'Success']
});
base.extend(io.prototype, {
	_onStateChange: function() {
		var readyState = this.xhr.readyState;
		if(readyState === 4) {
			this._complete = true;
			var res = new io.response(this.xhr, this.options.format, this.options.sanitizeJSON);

			// if complete, check for onFailure or onSuccess and fire function if available
			if(this.options.onFailure || this.options.onSuccess) {
				var successCode = (!this.xhr.status) ? 5 : (this.xhr.status >= 200 && this.xhr.status < 300) ? 6 : 5;
				(this.options['on'+io.Events[successCode]] || function() {})(res);
			}

			// if onEvent function is available for this state
			(this.options['on'+io.Events[readyState]] || function() {})(res);
		} else if(readyState > 1 && !((readyState == 4) && this._complete)) {
			(this.options['on'+io.Events[readyState]] || function() {})();
		}
	}
});

/**
 * HTML Templates that can be filled with object data.
 * @param template	{string}		The markup that will be used for the template.
 */
var Template = function(template) {
	this.template = template;
	return this.template;
}
base.extend(Template.prototype, {
	/**
	* Parse the data object into the template, replacing #{key} with key values.
	* @param object	{object}		Key/value pairs to parse into the template object.
	*/
	parse: function(object) {
		this.data = object;
		this.output = this.template;
		// match every instance of #{key} and replace it with what's in the data object
		this.output = this.output.replace(/#\{(\w+)\}/g, this._replaceCallback.bind(this));
		return this.output;
	},
	/**
	* Private method for parsing the template.
	* @param match1	{string}		Matches #{key}
	* @param match2	{string}		Matches key
	*/
	_replaceCallback: function(match1, match2) {
		return this.data[match2] || '';
	}
});

/**
 * Simplified element creation function
 * @param type		{string}		The type of element to create.
 * @param atts		{object}		Attributes to attached to the HTMLElement. (optional)
 * @param content	{string}		Set the innerHTML of the element. (optional)
 */
var Element = function(type, atts, content) {
	this.el = document.createElement(type);

	for(var attr in atts) {
		this.el.setAttribute(attr, atts[attr]);
	}
	
	this.el.innerHTML = content || '';
	return this.el;
};

base.extend(String.prototype, {
	/**
	* Check if the string is empty or whitespace only
	*/
	blank: function() {
		return /^\s*$/.test(this);
	},
	/**
	* Convert a string into an HTML NodeList
	*/
	toHTML: function() {
		var div = document.createElement('div');
		div.innerHTML = this;
		return div.childNodes;
	},
	/**
	* trim trailing whitespace or custom match from a string
	* @param match		{string}		Regular Expression string to trim. (optional)
	*/
	trim: function(match) {
		var re = new RegExp(match) || new RegExp(/\s+?/);
		return this.replace(re, '');
	}
});

base.extend(Event.prototype, {
	/**
	* Shortcut to preventDefault and stopPropagation on events.
	*/
	stop: function() {
		this.preventDefault();
		this.stopPropagation();
	}
});

base.extend(HTMLElement.prototype, {
	/**
	* Add a className to an element
	* @param className {string} Name of the class to add.
	*/
	addClass: function(className) {
		if(!this.hasClass(className)) {
			this.className += ' '+className.trim();
		}
		return this;
	},
	/**
	* Check if element has a given class name
	* @param className	{string}		Name of the class to test against.
	*/
	hasClass: function(className) {
		var re = new RegExp('(?:^|\\s+)'+className+'(?:\\s+|$)');
		return re.test(this.className);
	},
	/**
	* Remove a className from an element
	* @param className {string} Name of the class to remove.
	*/
	removeClass: function(className) {
		if(this.hasClass(className)) {
			var re = new RegExp('(?:^|\\s+)'+className+'(?:\\s+|$)');
			this.className = this.className.replace(re, ' ');
			// iterate through in case of multiple adjacent classes
			if(this.hasClass(className)) { this.removeClass(className); } else {
				this.className = this.className.trim();
			}
		}
		return this;
	},
	/**
	* Toggle a className on an element
	* @param className {string} Name of the class to remove.
	*/
	toggleClass: function(className) {
		if(this.hasClass(className)) {
			this.removeClass(className);
		} else {
			this.addClass(className);
		}
	},
	/**
	* Calculate the cumulative offset from the left and top of the document.
	* Accessible as array [x, y] or objects x||y
	*/
	getXY: function() {
		var valueX = 0, valueY = 0;
		var element = this;
		do {
			valueY += element.offsetTop	|| 0;
			valueX += element.offsetLeft || 0;
			element = element.offsetParent;
		} while (element);
		var offset = [valueX, valueY];
		offset.x = valueX, offset.y = valueY;
		
		return offset;
	},
	fire: function(eventName, memo) {
		base._fire(this, eventName, memo);
	}
});

base.extend(document, { 
	_loaded: false,
	fire: function(eventName, memo) {
		base._fire(this, eventName, memo);
	}
});

/**
 * Fire a custom 'dom:loaded' event using DOMContentLoaded if available, else use fallback
 * Safari has had native implementation since WebKit 525+
 *
 * Clear console calls if there is no console
 *
 * Create our magic query selector. Load in Sizzle if necessary.
 * $(selector) returns a NodeList
 * @param selector {string} CSS query of selectors.
 * @param context {HTMLElement} Context for the query. Limits scope of query.
 */
(function() {
	// fire the custom dom:loaded event
	var timer;
	function fireContentLoaded() {
		if(!document._loaded) {
			if(timer) { window.clearInterval(timer); }
		}
		document.fire('dom:loaded');
		document._loaded = true;
	}
	
	// make any failed console calls silent
	if(typeof console !== 'object') {
		console = { 
			log: function() {}, alert: function() {}, warn: function() {}, info: function() {},
			time: function() {}, timeEnd: function() {}, error: function() {}
		};
	}
	if(typeof document.querySelectorAll === 'function') {
		window.$ = function(selector, context) {
			context = (!!context) ? context : document;
			base.selectors = true;
			return context.querySelectorAll(selector);
		}
		document.addEventListener('DOMContentLoaded', fireContentLoaded, false); 

	} else {
		// note that at this time, Sizzle is not Internet Explorer compatible
		console.warn('Selectors API not available. Falling back on Sizzle query selector.');
		
		var sizzle = new Element('script', { type: 'text/javascript', src: base.sizzleSrc });
		sizzle.onload = function() {
			window.$ = Sizzle;
			if(base.browser.webkit && parseInt(base.browser.version) < 525) {
				console.info('DOMContentLoaded not available. Falling back on document.readyState.')
				timer = window.setInterval(function() {
					if(/loaded|complete/.test(document.readyState)) { fireContentLoaded(); }
				}, 0);
			} else {
				document.addEventListener('DOMContentLoaded', fireContentLoaded, false); 
			}
		};
		sizzle.onerror = function() {
			console.error('Horrible failure getting Sizzle. That sucks.');
		};
		document.getElementsByTagName('head')[0].appendChild(sizzle);
	}
})();
