
//empties an element and fills it with an object
jQuery.fn.fill = function(selector){
	$(selector).empty().append(this);
	return this;
};

//replaces an object completely
jQuery.fn.replace = function(selector){
	var target = $(selector);
	this.insertBefore(target);
	target.remove();
	return this;
};

//replaces an object completely
jQuery.fn.ready = function(delegate){
	delegate(this);
	return this;
};

/*
 * template (string or object)
 *	- "http://url.to.template"
 * 	- { url:"http://url.to.template", method:"post|get", data:{} }
 * 
 * externalData (null, string or object)
 *  - null (no external data source used)
 *  - "http://url.to.data"
 *  - { url:"http://url.to.data", method:"post|get", data:{} }
 *  
 * data (object)
 *  - { data }
 *  
 * error (delegate)
 *  - function(htmlResult, { data }) { ... }
 *  
 * format (delegate) -- returns jQueryObject
 *  - function(jQueryTemplate, { data }) { ... }
 *   
 */

//starts a template request
(function() {

	//replaces the default formatter	
	var _format = null;
	
	//quick function to get default values
	var _ = function(arg1, arg2, arg3, arg4, arg5){
		if (arg1 != null) { return arg1; }
		if (arg2 != null) { return arg2; }
		if (arg3 != null) { return arg3; }
		if (arg4 != null) { return arg4; }
		if (arg5 != null) { return arg5; }
		return null;
	};
	
	//function that actually starts the template request
	jQuery.template = function(settings){
		
		//if passing a replacement formatter, update and exit
		if ($.isFunction(settings)) {
			_format = settings;
			return;
		}
		
		//check if this was only a request for a url
		if (typeof(settings) == "string") {
			settings = { template: settings };
		}
		
		//check if a url was only provided for target
		if (typeof(settings.template) == "string") {
			settings.template = { url: settings.template }
		}
		
		//check for external data sources
		if (typeof(settings.externalData) == "string") {
			settings.externalData = { url: settings.externalData }
		}
		
		//create the actual object
		var self = {
			
			//handles forwarding method calls
			container:$(),
	
			//holds the object that will eventually be used		
			actual:null,
			
			//contains the actions waiting to be called
			queue:[],
			
			//prepares the container before returning it
			prepareContainer:function() {
				
				//make sure to forward methods to the queue
				self.forwardMethods();
				
			},
			
			//handles preparing method forwarding
			forwardMethods: function(){
				for (var item in self.container) {
					
					//assign each method to write to the queue instead of the actual object
					(function(method) {
						self.container[method] = function(p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14){
							
							//if the actual object exists, just pass through
							if (self.actual) {
								return self.actual[method](p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14);
							}
							
							//if not, queue the action instead
							self.queue.push({ method:method, args:[p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14] });
							
							//and return the container since it most likely
							//is the correct response type for a jQuery object
							return self.container;
							
						};
					})(item);
				}
			},
			
			//downloads the html content for the 
			download:{
				
				//tracks what data is actually expected to be loaded
				request:{
					template:false,
					data:false
				},
				
				//the downloaded content for the template
				result:{
					failed:false,
					html: null,
					data: {}
				},
				
				//gets the HTML content for the template
				template: function(){
				
					//start the download for the HTML content
					self.download.request.template = true;
					$.ajax({
						url: settings.template.url,
						data: _(settings.template.data, {}),
						dataType: _(settings.template.dataType, "html"),
						method: _(settings.template.method, "get"),
						complete: _(settings.template.complete, function(){ }),
						
						//errors get sent to custom error handling
						error: function(p1, p2, p3){
						
							//don't bother if this has already failed
							if (self.download.result.failed) { return; }
							self.download.result.failed = true;
							
							//if there is a handler 
							_(settings.error, function(){ })("template", p1, p2, p3);
							
						},
						
						//success checks for processing
						success: function(html){
							if (self.download.result.failed) { return; }
							self.download.result.html = html;
							self.createElement();
						}
					});
					
				},
				
				//handles downloading custom data
				data: function(){
					
					//clear the current data
					self.download.request.data = true;
					self.download.result.data = null;
					
					//start the download for the HTML content
					$.ajax({
						url: settings.externalData.url,
						data: _(settings.externalData.data, {}),
						dataType: _(settings.externalData.dataType, "json"),
						method: _(settings.externalData.method, "get"),
						complete: _(settings.externalData.complete, function(){ }),
						
						//errors get sent to custom error handling
						error: function(p1, p2, p3){
						
							//don't bother if this has already failed
							if (self.download.result.failed) { return; }
							self.download.result.failed = true;
							
							//if there is a handler 
							_(settings.error, function(){ })("data", p1, p2, p3);
							
						},
						
						//success checks for processing
						success: function(json){
							if (self.download.result.failed) { return; }
							self.download.result.data = json;
							self.createElement();
						}
					});
					
				}
				
			},
			
			//attempts to create the object
			createElement:function() {
				
				//if not both data is avaiable, exit
				if (!self.download.result.html) { return; }
				if (self.download.request.data && !self.download.result.data) { return; }
				
				//get the formatter to use
				var format = _(settings.format, _format, self.format);

				//build the content
				self.actual = format({
					html:self.download.result.html, 
					data:self.download.result.data
					}); 
				
				//catch up the queued methods
				self.processQueue();
				
			},
			
			//performs waiting actions inside of the queue
			processQueue:function() {
				var process = self.actual;
				$.each(self.queue, function(i, action) {
					process = process[action.method](
						action.args[0], action.args[1], action.args[2],
						action.args[3], action.args[4], action.args[5],
						action.args[6], action.args[7], action.args[8],
						action.args[9], action.args[10], action.args[11],
						action.args[12], action.args[13], action.args[14]
						);
				});
				self.actual = process;
			},
			
			//functionality for built in templating
			formatting:{
				
				//expression to match with
				match:/%{2}([^%{2}]+)%{2}/gi,
				
				//returns if this matches the default text matching
				isMatch:function(text) {
					return _(text, "").toString().match(self.formatting.match);
				},
				
				//handles finding the information for some text
				getDetail:function(text) {
					text = text.replace(self.formatting.match, function(match, val) { return val; });
					
					//get any modifiers for the phrase
					var modifier = null; 
					var not = false;
					text = text.replace(/^\!/, function(match, val) { not = true; return ""; });
					text = text.replace(/^(\w+):/, function(match, val) { modifier = val; return ""; });
						
					//create the information to track the string
					var inside = null;
					var current = [];
					var values = [];
					
					//methods for saving values
					var make = function(type) {
						
						//make sure there is really a value
						var value = $.trim(current.join(""));
						if (value == "") { return; }
						
						//add the value
						values.push({ type:type, value:current.join("") });
						
						//reset the tracking
						current = []; 
						inside = null;
						 
					};
	
					//check for values				
					for(var i = 0; i < text.length; i++) {
						
						//get the character being worked with
						var letter = text.charAt(i);
						
						//if this is a string, start it tracking
						if (inside == null && (letter == '"' || letter == "'")) {
							inside = letter;
							continue;
						}
						//if the end of a string
						else if(inside && inside == letter) {
							make("string");
						}
						//if this is a delimeter and not in a string
						else if(!inside && letter == ",") {
							make("var");
						}
						//otherwise, just another character
						else {
							current.push(letter);
						}
						
					}
					
					//check for any remaining values
					if (current.length > 0) {
						inside ? make("string") : make("var");
					}
					
					//return the results
					return {
						not:not,
						identity:values.length == 1 ? values[0].value : null,
						values:values,
						modifier:modifier
					};
					
				},
				
				//checks a position to verify if it should be accepted or not
				checkModifier:function(value, position) {
					
					//if this is checking a string, format into a detail object
					if (typeof(value) == "string") {
						value = $.trim(_(value, ""));
						position = _(position, {});
						
						//format the value
						value = value.replace(self.formatting.match, function(match, val) { return val; });
						value = self.formatting.getDetail(["$", value, ":$"].join(""));
					}
					
					//check the positions 
					value.modifier = _(value.modifier, "").toString();
					var matches = (
						(value.modifier.match(/^first$/i) && position.first) ||
						(value.modifier.match(/^last$/i) && position.last) ||
						(value.modifier.match(/^odd$/i) && position.odd) ||
						(value.modifier.match(/^even$/i) && position.even)
						);
	
					//return if this is usable or not
					return value.not ? !matches : matches;
				},
				
				//parses a block of text for matches
				parse:function(text, data, position){
					var show = text.match(/\$type\$/i);
					
					//find the matches
					return text.replace(self.formatting.match, function(str, match){
											
						//get the detail for this request
						var detail = self.formatting.getDetail(match);
						
						//check for any special rules
						if (detail.modifier && !self.formatting.checkModifier(detail, position)) { return ""; }
						
						//get each value to display
						var value = [];
						$.each(detail.values, function(i, v) {
							if (v.type == "string") {
								value.push(v.value);
							}
							else if (v.type == "var") {
								value.push(data[v.value]);
							}
						});
						
						//get the content and determine how to display it
						return value.join("");
						
					});
					
				},
				
				//begins evaluating elements to populate information
				evaluate:function(element, data, position){
					
					//if no data is provided for the template
					if (!data || typeof(data) != "object" || data.length == 0) { 
						return element; 
					}
					
					//replace any content found in the elements
					$.each(element.children(), function(i, v) {
						var item = $(v);
						
						//check if this has a rendering flag
						if (item.attr("if")) {
							if (self.formatting.checkModifier(item.attr("if"), position)) {
								item.removeAttr("if");
							}
							else {
								item.remove();
							}
						}
						
						//if this is an iterated element
						if (item.attr("each")) {
							
							//get the new baseline for the data
							var detail = self.formatting.getDetail(item.attr("each"));
							var collection = data[detail.identity];
							
							//if this isn't usable, give up
							if (collection == null || !collection.length) {
								if (settings.complain) {
									throw ["Exception: ", detail.identity, " is not a usable data source for a collection."].join("");
								}
								else {
									return element;
								}
							}
							
							//clone and create the object multiple times
							item.removeAttr("each");
							$.each(collection, function(j, w) {
								
								//build the child information
								var child = item.clone();
								var pos = { first:j==0, last:j==collection.length-1, even:j%2==0, odd:j%2!=0 };
								self.formatting.evaluate(child, w, pos);
								
								//add it to the parent
								child.insertBefore(item);
								
							});
							
							//finally, clear the iterated element
							item.remove();
							
						}
						//otherwise, just check the children
						else {
							self.formatting.evaluate(item, data, {});
						}
						
					});
					
					//update each of the elements
					var attr = element.get(0).attributes;
					for (var i = 0; i < attr.length; i++) {
						var val = attr[i].value;
						if (self.formatting.isMatch(val)) {
							attr[i].value = self.formatting.parse(val, data, position);
						}
					}
					
					//after evaluation, perform any text matching
					element.html(self.formatting.parse(element.html(), data, position));
					
				}
				
			},
			
			//handles creating the templated content
			format:function(params) {
			
				//start at the top child
				var element = $(params.html);
				self.formatting.evaluate(element, params.data);
				
				//return the final, formatted element
				return element;
	
			},
			
			//prepares the jQuery object for action
			init: function(){
				
				//get the container ready to be used
				self.prepareContainer();
				
				//assign the default data (if any)
				self.download.result.data = _(settings.data, null);
				
				//execute the download requests
				self.download.template();
				if (settings.externalData) { self.download.data(); }
				
			}
			
		};	
		
		//prepare the object
		self.init();
		
		//and return the object handling the forwarding
		return self.container;
		
	};

})();