(function($) {

$.extend({pwTyper: {

wrap: function(s) {

// remove legacy span.pwtext elements
s = s.replace(/(<span[^>]*?class="[^"]*pwText[^"]*>)([^<]*)(<\/span>)/g, '$2');

// wraps all text nodes in a span.pwtext element
s = s.replace(/(>|^(?!<))([^<]+)(<|(?!>)$)/g, '$1<span class="pwText">$2</span>$3');

// groups special characters such as &amp; into a span.pwSpecial element
// only match special characters within existing .pwText elements (not in attributes)
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 span.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) {
var callback = G.thisElement.data("callback");
if (callback) {
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).removeClass('pwHidden');
}

// 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) {
var callback = G.thisElement.data("callback");
if (callback) {
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);
},

createCSS: function (selector, declaration) {

// test for IE
var ua = navigator.userAgent.toLowerCase();
var isIE = (/msie/.test(ua)) && !(/opera/.test(ua)) && (/win/.test(ua));

// create the style node for all browsers, if it doesn't already exists
var style_node = document.getElementById('pwTyperStyles');
if ( !style_node ) {
style_node = document.createElement("style");
style_node.setAttribute("type", "text/css");
style_node.setAttribute("media", "screen");
style_node.setAttribute("id", "pwTyperStyles");
}

// if the rule doesn't already exist, add it
if ( style_node.innerHTML.indexOf(selector + " {" + declaration + "}") === -1 ) {

// append a rule for good browsers
if (!isIE) style_node.appendChild(document.createTextNode(selector + " {" + declaration + "}\n"));

// append the style node
document.getElementsByTagName("head")[0].appendChild(style_node);

// use alternative methods for IE
if (isIE && document.styleSheets && document.styleSheets.length > 0) {
var last_style_node = document.styleSheets[document.styleSheets.length - 1];
if (typeof(last_style_node.addRule) == "object") last_style_node.addRule(selector, declaration);
}
}
}
}});


$.fn.extend({

stopTyper: function() {
clearInterval(this.data("int"));
return this;
},

resumeTyper: function() {
this.data('func').call($.pwTyper, this.data("G"));
return this;
},

finishTyper: function() {
clearInterval(this.data("int"));
if (this.data('func') == $.pwTyper.type) {
this.html(this.data("content"));
} else {
this.empty();
}

var callback = this.data("callback");
if (callback) {
callback.call();
}

return this;
},

type: function(options) {

// add CSS styles if they haven't already been added
$.pwTyper.createCSS('.pwHidden', 'display:none;');

clearInterval(this.data("int"));

// Default settings
var settings = {
minInterval: 30,
maxInterval: 90
};

// 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);
G.thisElement.data("content", G.content);

// 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).addClass("pwHidden").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) }, settings.delay));
} else {
$.pwTyper.type(G);
}
});
},

untype: function(options) {

// add CSS styles if they haven't already been added
$.pwTyper.createCSS('.pwHidden', 'display:none;');

clearInterval(this.data("int"));

// Default settings
var settings = {
minInterval: 30,
maxInterval: 90
};

// 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) }, settings.delay));
} else {
$.pwTyper.untype(G);
}
});

}
});

})(jQuery);
