(function($) {
	var locationWrapper = {
		put: function(hash, win) {
			(win || window).location.hash = this.encoder(hash);
		},
		get: function(win) {
			var hash = ((win || window).location.hash).replace(/^#/, '');
			try {
				return $.browser.mozilla ? hash : decodeURIComponent(hash);
			}
			catch (error) {
				return hash;
			}
		},
		encoder: encodeURIComponent
	};

	var iframeWrapper = {
		id: "__jQuery_history",
		init: function() {
			var html = '<iframe id="'+ this.id +'" style="display:none" src="javascript:false;" />';
			$("body").prepend(html);
			return this;
		},
		_document: function() {
			return $("#"+ this.id)[0].contentWindow.document;
		},
		put: function(hash) {
			var doc = this._document();
			doc.open();
			doc.close();
			locationWrapper.put(hash, doc);
		},
		get: function() {
			return locationWrapper.get(this._document());
		}
	};

	function initObjects(options) {
		options = $.extend({
				unescape: false
			}, options || {});

		locationWrapper.encoder = encoder(options.unescape);

		function encoder(unescape_) {
			if(unescape_ === true) {
				return function(hash){ return hash; };
			}
			if(typeof unescape_ == "string" &&
			   (unescape_ = partialDecoder(unescape_.split("")))
			   || typeof unescape_ == "function") {
				return function(hash) { return unescape_(encodeURIComponent(hash)); };
			}
			return encodeURIComponent;
		}

		function partialDecoder(chars) {
			var re = new RegExp($.map(chars, encodeURIComponent).join("|"), "ig");
			return function(enc) { return enc.replace(re, decodeURIComponent); };
		}
	}

	var implementations = {};

	implementations.base = {
		type: undefined,
		callbacks: [],
		binds: {},

		check: function() {},
		load: function(hash) {},
		trigger: function(hash) {
			if (self.callbacks.length) {
				for (var i = 0 ; i < self.callbacks.length ; i++) {
					self.callbacks[i](hash);
				}
			}
			if (self.binds[hash]) {
				self.binds[hash]();
			}
		},
		bind: function(str, callback) {
			if (callback) {
				self.binds[str] = callback;
			}
			else {
				self.callbacks[self.callbacks.length] = str;
			}
		},
		init: function(options) {
			initObjects(options);
			self._options = options;
			self._init();
		},

		_init: function() {},
		_options: {}
	};

	implementations.timer = {
		_appState: undefined,
		_init: function() {
			var current_hash = locationWrapper.get();
			self._appState = current_hash;
			self.trigger(current_hash);
			setInterval(self.check, 100);
		},
		check: function() {
			var current_hash = locationWrapper.get();
			if(current_hash != self._appState) {
				self._appState = current_hash;
				self.trigger(current_hash);
			}
		},
		load: function(hash) {
			if(hash != self._appState) {
				locationWrapper.put(hash);
				self._appState = hash;
				self.trigger(hash);
			}
		}
	};

	implementations.iframeTimer = {
		_appState: undefined,
		_init: function() {
			var current_hash = locationWrapper.get();
			self._appState = current_hash;
			iframeWrapper.init().put(current_hash);
			self.trigger(current_hash);
			setInterval(self.check, 100);
		},
		check: function() {
			var iframe_hash = iframeWrapper.get(),
				location_hash = locationWrapper.get();

			if (location_hash != iframe_hash) {
				if (location_hash == self._appState) {	// user used Back or Forward button
					self._appState = iframe_hash;
					locationWrapper.put(iframe_hash);
					self.trigger(iframe_hash);
				} else {							  // user loaded new bookmark
					self._appState = location_hash;
					iframeWrapper.put(location_hash);
					self.trigger(location_hash);
				}
			}
		},
		load: function(hash) {
			if(hash != self._appState) {
				locationWrapper.put(hash);
				iframeWrapper.put(hash);
				self._appState = hash;
				self.trigger(hash);
			}
		}
	};

	implementations.hashchangeEvent = {
		_init: function() {
			self.trigger(locationWrapper.get());
			$(window).bind('hashchange', self.check);
		},
		check: function() {
			self.trigger(locationWrapper.get());
		},
		load: function(hash) {
			locationWrapper.put(hash);
		}
	};

	var self = $.extend({}, implementations.base);

	if($.browser.msie && ($.browser.version < 8 || document.documentMode < 8)) {
		self.type = 'iframeTimer';
	}
	else if("onhashchange" in window) {
		self.type = 'hashchangeEvent';
	}
	else {
		self.type = 'timer';
	}

	$.extend(self, implementations[self.type]);
	$.history = self;
	
})(jQuery);

