(function($) {
	
	$.extend({pwTyper: {
	
		wrap: function(s) {
			
			// remove legacy pwtext elements
			s = s.replace(/(<span[^<>]*class="[^"]*pwText[^<>]*>)([^<>]*)(<\/span>)/g, '$2');
	
			// wraps all text nodes in a pwText pwtext element 
			s = s.replace(/(>|^(?!<))([^<>]*)(<|(?!>)$)/g, '$1<span class="pwText">$2</span>$3');
			
			// groups special characters such as &amp; into their own pwtext element, but not the ones in URLs
			s = s.replace(/<span class="pwText">[^<>]*&\S*;[^<>]*<\/span>/g, function(m) { return m.replace(/(&[^&; ]*;)/g, '</span><span class="pwSpecial">$1</span><span class="pwText">'); });
			
			// remove empty pwText tags and wrap in a pwText pwtext if needed
			s = s.replace(/<span class="pwText">[\n\r]*<\/span>/g, "");
			
			return s;
		},
		
		// adds the next letter 
		type: function(G) {
		
			// increase dataIndex, which moves to the next element
			if (G.charCount > G.data[G.dataIndex].count) {
				G.dataIndex++;
			}
			
			// If typing is complete, restore the original state
			if (G.dataIndex == G.dataLength) {
				G.thisElement.data("finished", true);
				G.thisElement.html(G.content);
				
				
				// if typing is complete for ALL elements
				if (G.thisElement.data("callback")) {
					var finished = true;
					$(G.thisElement.data("get")).each(function() {
						if (!$(this).data("finished")) { finished = false; }														 
					});
					if (finished) { G.thisElement.data("callback").call(); }
				}
				return false;
			}
	
			// show the current element and all previous elements which are still hidden
			var newOrder = G.data[G.dataIndex].element.data("order");
			for (G.order; G.order <= newOrder; G.order++) {
				G.thisElement.find('.order-'+G.order).show();		
			}
			
			// type the next character
			G.data[G.dataIndex].element.html(G.data[G.dataIndex].text.substr(0, G.charCount - ((G.dataIndex > 0) ? G.data[G.dataIndex-1].count : 0)));
			G.delay = Math.round(G.minInterval + (Math.random() * (G.maxInterval - G.minInterval)));
			G.thisElement.data("int", setTimeout(function() { $.pwTyper.type(G) }, G.delay));			
			
			G.charCount++;	
			
			// Stores the G data in the element to use in pause and stop functions
			G.thisElement.data("G", G);
		},
		
		// removes the last letter 
		untype: function(G) {
						
			// increase dataIndex, which moves to the next element
			if (G.dataIndex > 0 && G.charCount <= G.data[G.dataIndex - 1].count) {
				G.dataIndex--;
			}
			
			// If untyping is complete
			if (G.charCount === 0) {
				G.thisElement.data("finished", true);
				G.thisElement.html("");
				
				// if untyping is complete for ALL elements
				if (G.thisElement.data("callback") && $(G.thisElement.data("get")).data('finished')) {
					var finished = true;
					$(G.thisElement.data("get")).each(function() {
						if (!$(this).data("finished")) { finished = false; }														 
					});
					if (finished) { G.thisElement.data("callback").call(); }
				}
				return false;
			}
	
			// show the current element and all previous elements which are still hidden
			var newOrder = G.data[G.dataIndex].element.data("order");
			for (G.order; G.order > newOrder; G.order--) {
				G.thisElement.find('.order-'+G.order).remove();		
			}
			
			// type the next character
			G.data[G.dataIndex].element.html(G.data[G.dataIndex].text.substr(0, G.charCount - 1 - ((G.dataIndex > 0) ? G.data[G.dataIndex-1].count : 0)));
			G.delay = Math.round(G.minInterval + (Math.random() * (G.maxInterval - G.minInterval)));
			G.thisElement.data("int", setTimeout(function() { $.pwTyper.untype(G) }, G.delay));			
			
			G.charCount--;
			
			// Stores the G data in the element to use in pause and stop functions
			G.thisElement.data("G", G);
		}
	}});
	
	
	$.fn.extend({
	
		stopTyper: function() {
			clearInterval(this.data("int"));
			return this;
		},
		
		resumeTyper: function() {
			this.data('func').call($.pwTyper, this.data("G"));
			return this;
		},
		

		
		type: function(options) {
			
			clearInterval(this.data("int"));
			
			// Default settings
			var settings = {
				minInterval: 30,
				maxInterval: 60
			};
			
			// Processing settings
			settings = jQuery.extend(settings, options || {});
			
			this.data("func", $.pwTyper.type);
			this.data("get", this.get());
			this.data("callback", (settings.callback) ? settings.callback : null);
			
			
			return this.each(function() {
				
				var G = {
					charCount: 0,
					charTotal: 0,
					data: [],
					dataLength: 0,
					dataIndex: 0,
					thisElement: $(this),
					order: 0, 
					delay: 0,
					newText: "",
					content: "",
					minInterval: settings.minInterval,
					maxInterval: settings.maxInterval
				};
			
				if (!settings.content) {
					G.content = G.thisElement.html();	
				} else if (settings.content instanceof jQuery) {
					G.content = $(settings.content).html();
				} else {
					G.content = settings.content;	
				}
				G.thisElement.data("finished", false);
				
				// wraps all text nodes in a pwText span element 
				G.newText = $.pwTyper.wrap(G.content);
				
								
				
				// Creates an order for all elements to progressively show them as the typing happens
				G.thisElement.html(G.newText).find('*').each(function(i) {
					$(this).hide().data("order", i).addClass("order-" + i);									
				});
	
				// empties the text from the span elements and stores it in the 'data' variable
				G.thisElement.find('.pwText').each(function(i) {
					G.data[i] = {order:$(this).data("order"), text: $(this).html(), element: $(this), count: (i > 0) ? $(this).html().length + G.data[i-1].count : $(this).html().length};
					$(this).empty();
					
				});
				
				
				
				G.dataLength = G.data.length;
				G.charTotal = G.data[G.dataLength-1].count;
				
	
				
				// if a time is specified, calculate the delay
				if (settings.time) {
					G.delay = Math.floor(settings.time / G.charTotal);
					if (G.delay === 0) { G.delay = 1; }
					if (settings.deviation) {
						if (settings.deviation > 1) { settings.deviation = 1; }
						G.minInterval = Math.round(G.delay * (1 - settings.deviation));
						G.maxInterval = G.delay + (G.delay - G.minInterval);
						if (G.minInterval === 0) { G.minInterval = 1; }
					} else {
						G.minInterval = G.delay;
						G.maxInterval = G.delay;
					}
				}
				
				if (settings.delay) {
					G.thisElement.data("int", setTimeout( function() { $.pwTyper.type(G) }, G.delay));
				} else {
					$.pwTyper.type(G);	
				}
			});
			
		},
	
		untype: function(options) {
			
			clearInterval(this.data("int"));
			
			// Default settings
			var settings = {
				minInterval: 30,
				maxInterval: 60
			};
			
			// Processing settings
			settings = jQuery.extend(settings, options || {});
			
			this.data("func", $.pwTyper.untype);
			this.data("get", this.get());
			this.data("callback", (settings.callback) ? settings.callback : null);
		
			
			return this.each(function() {
			
				var G = {
					charCount: 0,
					charTotal: 0,
					data: [],
					dataLength: 0,
					dataIndex: 0,
					thisElement: $(this),
					order: 0,
					delay: 0,
					newText:"",
					content: $(this).html(),
					minInterval: settings.minInterval, 
					maxInterval: settings.maxInterval
				};
				G.thisElement.data("finished", false);
				
				// wraps all text nodes in a pwText span element 
				G.newText = $.pwTyper.wrap(G.content);
					
				// Creates an order for all elements to progressively show them as the typing happens
				G.thisElement.html(G.newText).find('*').each(function(i) {
					$(this).data("order", i).addClass("order-" + i);									
				});
				
				// takes the text from the span elements and stores it in the 'data' variable
				G.thisElement.find('.pwText').each(function(i) {
					G.data[i] = {order:$(this).data("order"), text:$(this).html(), element:$(this), count: (i > 0) ? $(this).html().length + G.data[i-1].count : $(this).html().length};
				});
				
				if (G.data.length > 0) {
					G.dataIndex = G.data.length - 1;
					G.charTotal = G.data[G.dataIndex].count;
					G.charCount = G.charTotal;
					G.order = G.data[G.dataIndex].element.data("order");
					
					// if a time is specified, calculate the delay
					if (settings.time) {
						G.delay = Math.floor(settings.time / G.charTotal);
						if (G.delay === 0) { G.delay = 1; }
						if (settings.deviation) {
							if (settings.deviation > 1) { settings.deviation = 1; }
							G.minInterval = Math.round(G.delay * (1 - settings.deviation));
							G.maxInterval = G.delay + (G.delay - G.minInterval);
							if (G.minInterval === 0) { G.minInterval = 1; }
						} else {
							G.minInterval = G.delay;
							G.maxInterval = G.delay;
						}
					}
				}
				
				if (settings.delay) {
					G.thisElement.data("int", setTimeout(function() { $.pwTyper.untype(G) }, G.delay));
				} else {
					$.pwTyper.untype(G);	
				}
			});
			
		}
	});
		
})(jQuery);
