/**
 * History.js Core
 * @author Benjamin Arthur Lupton <contact@balupton.com>
 * @copyright 2010-2011 Benjamin Arthur Lupton <contact@balupton.com>
 * @license New BSD License <http://creativecommons.org/licenses/BSD/>
 */

(function (window, undefined) {
	"use strict";

	// --------------------------------------------------------------------------
	// Initialise

	// Localise Globals
	var 
		console = window.console || undefined, // Prevent a JSLint complain
		document = window.document, // Make sure we are using the correct document
		navigator = window.navigator, // Make sure we are using the correct navigator
		amplify = window.amplify || false, // Amplify.js
		setTimeout = window.setTimeout,
		clearTimeout = window.clearTimeout,
		setInterval = window.setInterval,
		clearInterval = window.clearInterval,
		JSON = window.JSON,
		History = window.History = window.History || {}, // Public History Object
		history = window.history; // Old History Object

	// MooTools Compatibility
	JSON.stringify = JSON.stringify || JSON.encode;
	JSON.parse = JSON.parse || JSON.decode;

	// Check Existence
	if (typeof History.init !== 'undefined') {
		throw new Error('History.js Core has already been loaded...');
	}

	// Initialise History
	History.init = function () {
		// Check Load Status of Adapter
		if (typeof History.Adapter === 'undefined') {
			return false;
		}

		// Check Load Status of Core
		if (typeof History.initCore !== 'undefined') {
			History.initCore();
		}

		// Check Load Status of HTML4 Support
		if (typeof History.initHtml4 !== 'undefined') {
			History.initHtml4();
		}

		// Return true
		return true;
	};

	// --------------------------------------------------------------------------
	// Initialise Core

	// Initialise Core
	History.initCore = function () {
		// Initialise
		if (typeof History.initCore.initialized !== 'undefined') {
			// Already Loaded
			return false;
		}
		else {
			History.initCore.initialized = true;
		}

		// ----------------------------------------------------------------------
		// Options

		/**
		* History.options
		* Configurable options
		*/
		History.options = History.options || {};

		/**
		* History.options.hashChangeInterval
		* How long should the interval be before hashchange checks
		*/
		History.options.hashChangeInterval = History.options.hashChangeInterval || 100;

		/**
		* History.options.safariPollInterval
		* How long should the interval be before safari poll checks
		*/
		History.options.safariPollInterval = History.options.safariPollInterval || 500;

		/**
		* History.options.doubleCheckInterval
		* How long should the interval be before we perform a double check
		*/
		History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500;

		/**
		* History.options.storeInterval
		* How long should we wait between store calls
		*/
		History.options.storeInterval = History.options.storeInterval || 1000;

		/**
		* History.options.busyDelay
		* How long should we wait between busy events
		*/
		History.options.busyDelay = History.options.busyDelay || 250;

		/**
		* History.options.debug
		* If true will enable debug messages to be logged
		*/
		History.options.debug = History.options.debug || false;

		/**
		* History.options.initialTitle
		* What is the title of the initial state
		*/
		History.options.initialTitle = History.options.initialTitle || document.title;


		// ----------------------------------------------------------------------
		// Interval record

		/**
		* History.intervalList
		* List of intervals set, to be cleared when document is unloaded.
		*/
		History.intervalList = [];

		/**
		* History.clearAllIntervals
		* Clears all setInterval instances.
		*/
		History.clearAllIntervals = function () {
			var i, il = History.intervalList;
			if (typeof il !== "undefined" && il !== null) {
				for (i = 0; i < il.length; i++) {
					clearInterval(il[i]);
				}
				History.intervalList = null;
			}
		};
		History.Adapter.bind(window, "beforeunload", History.clearAllIntervals);
		History.Adapter.bind(window, "unload", History.clearAllIntervals);


		// ----------------------------------------------------------------------
		// Debug

		/**
		* History.debug(message,...)
		* Logs the passed arguments if debug enabled
		*/
		History.debug = function () {
			if ((History.options.debug || false)) {
				History.log.apply(History, arguments);
			}
		};

		/**
		* History.log(message,...)
		* Logs the passed arguments
		*/
		History.log = function () {
			// Prepare
			var 
				consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'),
				textarea = document.getElementById('log'),
				message,
				i, n
				;

			// Write to Console
			if (consoleExists) {
				var args = Array.prototype.slice.call(arguments);
				message = args.shift();
				if (typeof console.debug !== 'undefined') {
					console.debug.apply(console, [message, args]);
				}
				else {
					console.log.apply(console, [message, args]);
				}
			}
			else {
				message = ("\n" + arguments[0] + "\n");
			}

			// Write to log
			for (i = 1, n = arguments.length; i < n; ++i) {
				var arg = arguments[i];
				if (typeof arg === 'object' && typeof JSON !== 'undefined') {
					try {
						arg = JSON.stringify(arg);
					}
					catch (Exception) {
						// Recursive Object
					}
				}
				message += "\n" + arg + "\n";
			}

			// Textarea
			if (textarea) {
				textarea.value += message + "\n-----\n";
				textarea.scrollTop = textarea.scrollHeight - textarea.clientHeight;
			}
			// No Textarea, No Console
			else if (!consoleExists) {
				alert(message);
			}

			// Return true
			return true;
		};

		// ----------------------------------------------------------------------
		// Emulated Status

		/**
		* History.getInternetExplorerMajorVersion()
		* Get's the major version of Internet Explorer
		* @return {integer}
		* @license Public Domain
		* @author Benjamin Arthur Lupton <contact@balupton.com>
		* @author James Padolsey <https://gist.github.com/527683>
		*/
		History.getInternetExplorerMajorVersion = function () {
			var result = History.getInternetExplorerMajorVersion.cached =
					(typeof History.getInternetExplorerMajorVersion.cached !== 'undefined')
				? History.getInternetExplorerMajorVersion.cached
				: (function () {
					var v = 3,
								div = document.createElement('div'),
								all = div.getElementsByTagName('i');
					while ((div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->') && all[0]) { }
					return (v > 4) ? v : false;
				})()
				;
			return result;
		};

		/**
		* History.isInternetExplorer()
		* Are we using Internet Explorer?
		* @return {boolean}
		* @license Public Domain
		* @author Benjamin Arthur Lupton <contact@balupton.com>
		*/
		History.isInternetExplorer = function () {
			var result =
				History.isInternetExplorer.cached =
				(typeof History.isInternetExplorer.cached !== 'undefined')
					? History.isInternetExplorer.cached
					: Boolean(History.getInternetExplorerMajorVersion())
				;
			return result;
		};

		/**
		* History.emulated
		* Which features require emulating?
		*/
		History.emulated = {
			pushState: !Boolean(
				window.history && window.history.pushState && window.history.replaceState
				&& !(
					(/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */
					|| (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */
				)
			),
			hashChange: Boolean(
				!(('onhashchange' in window) || ('onhashchange' in document))
				||
				(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8)
			)
		};

		/**
		* History.enabled
		* Is History enabled?
		*/
		History.enabled = !History.emulated.pushState;

		/**
		* History.bugs
		* Which bugs are present
		*/
		History.bugs = {
			/**
			* Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call
			* https://bugs.webkit.org/show_bug.cgi?id=56249
			*/
			setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)),

			/**
			* Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions
			* https://bugs.webkit.org/show_bug.cgi?id=42940
			*/
			safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)),

			/**
			* MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function)
			*/
			ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8),

			/**
			* MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event
			*/
			hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7)
		};

		/**
		* History.isEmptyObject(obj)
		* Checks to see if the Object is Empty
		* @param {Object} obj
		* @return {boolean}
		*/
		History.isEmptyObject = function (obj) {
			for (var name in obj) {
				return false;
			}
			return true;
		};

		/**
		* History.cloneObject(obj)
		* Clones a object
		* @param {Object} obj
		* @return {Object}
		*/
		History.cloneObject = function (obj) {
			var hash, newObj;
			if (obj) {
				hash = JSON.stringify(obj);
				newObj = JSON.parse(hash);
			}
			else {
				newObj = {};
			}
			return newObj;
		};

		// ----------------------------------------------------------------------
		// URL Helpers

		/**
		* History.getRootUrl()
		* Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com"
		* @return {String} rootUrl
		*/
		History.getRootUrl = function () {
			// Create
			var rootUrl = document.location.protocol + '//' + (document.location.hostname || document.location.host);
			if (document.location.port || false) {
				rootUrl += ':' + document.location.port;
			}
			rootUrl += '/';

			// Return
			return rootUrl;
		};

		/**
		* History.getBaseHref()
		* Fetches the `href` attribute of the `<base href="...">` element if it exists
		* @return {String} baseHref
		*/
		History.getBaseHref = function () {
			// Create
			var 
				baseElements = document.getElementsByTagName('base'),
				baseElement = null,
				baseHref = '';

			// Test for Base Element
			if (baseElements.length === 1) {
				// Prepare for Base Element
				baseElement = baseElements[0];
				baseHref = baseElement.href.replace(/[^\/]+$/, '');
			}

			// Adjust trailing slash
			baseHref = baseHref.replace(/\/+$/, '');
			if (baseHref) baseHref += '/';

			// Return
			return baseHref;
		};

		/**
		* History.getBaseUrl()
		* Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first)
		* @return {String} baseUrl
		*/
		History.getBaseUrl = function () {
			// Create
			var baseUrl = History.getBaseHref() || History.getBasePageUrl() || History.getRootUrl();

			// Return
			return baseUrl;
		};

		/**
		* History.getPageUrl()
		* Fetches the URL of the current page
		* @return {String} pageUrl
		*/
		History.getPageUrl = function () {
			// Fetch
			var 
				State = History.getState(false, false),
				stateUrl = (State || {}).url || document.location.href;

			// Create
			var pageUrl = stateUrl.replace(/\/+$/, '').replace(/[^\/]+$/, function (part, index, string) {
				return (/\./).test(part) ? part : part + '/';
			});

			// Return
			return pageUrl;
		};

		/**
		* History.getBasePageUrl()
		* Fetches the Url of the directory of the current page
		* @return {String} basePageUrl
		*/
		History.getBasePageUrl = function () {
			// Create
			var basePageUrl = document.location.href.replace(/[#\?].*/, '').replace(/[^\/]+$/, function (part, index, string) {
				return (/[^\/]$/).test(part) ? '' : part;
			}).replace(/\/+$/, '') + '/';

			// Return
			return basePageUrl;
		};

		/**
		* History.getFullUrl(url)
		* Ensures that we have an absolute URL and not a relative URL
		* @param {string} url
		* @param {Boolean} allowBaseHref
		* @return {string} fullUrl
		*/
		History.getFullUrl = function (url, allowBaseHref) {
			// Prepare
			var fullUrl = url, firstChar = url.substring(0, 1);
			allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref;

			// Check
			if (/[a-z]+\:\/\//.test(url)) {
				// Full URL
			}
			else if (firstChar === '/') {
				// Root URL
				fullUrl = History.getRootUrl() + url.replace(/^\/+/, '');
			}
			else if (firstChar === '#') {
				// Anchor URL
				fullUrl = History.getPageUrl().replace(/#.*/, '') + url;
			}
			else if (firstChar === '?') {
				// Query URL
				fullUrl = History.getPageUrl().replace(/[\?#].*/, '') + url;
			}
			else {
				// Relative URL
				if (allowBaseHref) {
					fullUrl = History.getBaseUrl() + url.replace(/^(\.\/)+/, '');
				} else {
					fullUrl = History.getBasePageUrl() + url.replace(/^(\.\/)+/, '');
				}
				// We have an if condition above as we do not want hashes
				// which are relative to the baseHref in our URLs
				// as if the baseHref changes, then all our bookmarks
				// would now point to different locations
				// whereas the basePageUrl will always stay the same
			}

			// Return
			return fullUrl.replace(/\#$/, '');
		};

		/**
		* History.getShortUrl(url)
		* Ensures that we have a relative URL and not a absolute URL
		* @param {string} url
		* @return {string} url
		*/
		History.getShortUrl = function (url) {
			// Prepare
			var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl();

			// Trim baseUrl
			if (History.emulated.pushState) {
				// We are in a if statement as when pushState is not emulated
				// The actual url these short urls are relative to can change
				// So within the same session, we the url may end up somewhere different
				shortUrl = shortUrl.replace(baseUrl, '');
			}

			// Trim rootUrl
			shortUrl = shortUrl.replace(rootUrl, '/');

			// Ensure we can still detect it as a state
			if (History.isTraditionalAnchor(shortUrl)) {
				shortUrl = './' + shortUrl;
			}

			// Clean It
			shortUrl = shortUrl.replace(/^(\.\/)+/g, './').replace(/\#$/, '');

			// Return
			return shortUrl;
		};

		// ----------------------------------------------------------------------
		// State Storage

		/**
		* History.store
		* The store for all session specific data
		*/
		History.store = amplify ? (amplify.store('History.store') || {}) : {};
		History.store.idToState = History.store.idToState || {};
		History.store.urlToId = History.store.urlToId || {};
		History.store.stateToId = History.store.stateToId || {};

		/**
		* History.idToState
		* 1-1: State ID to State Object
		*/
		History.idToState = History.idToState || {};

		/**
		* History.stateToId
		* 1-1: State String to State ID
		*/
		History.stateToId = History.stateToId || {};

		/**
		* History.urlToId
		* 1-1: State URL to State ID
		*/
		History.urlToId = History.urlToId || {};

		/**
		* History.storedStates
		* Store the states in an array
		*/
		History.storedStates = History.storedStates || [];

		/**
		* History.savedStates
		* Saved the states in an array
		*/
		History.savedStates = History.savedStates || [];

		/**
		* History.getState()
		* Get an object containing the data, title and url of the current state
		* @param {Boolean} friendly
		* @param {Boolean} create
		* @return {Object} State
		*/
		History.getState = function (friendly, create) {
			// Prepare
			if (typeof friendly === 'undefined') { friendly = true; }
			if (typeof create === 'undefined') { create = true; }

			// Fetch
			var State = History.getLastSavedState();

			// Create
			if (!State && create) {
				State = History.createStateObject();
			}

			// Adjust
			if (friendly) {
				State = History.cloneObject(State);
				State.url = State.cleanUrl || State.url;
			}

			// Return
			return State;
		};

		/**
		* History.getIdByState(State)
		* Gets a ID for a State
		* @param {State} newState
		* @return {String} id
		*/
		History.getIdByState = function (newState) {

			// Fetch ID
			var id = History.extractId(newState.url);
			if (!id) {
				// Find ID via State String
				var str = History.getStateString(newState);
				if (typeof History.stateToId[str] !== 'undefined') {
					id = History.stateToId[str];
				}
				else if (typeof History.store.stateToId[str] !== 'undefined') {
					id = History.store.stateToId[str];
				}
				else {
					// Generate a new ID
					while (true) {
						id = String(Math.floor(Math.random() * 1000));
						if (typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined') {
							break;
						}
					}

					// Apply the new State to the ID
					History.stateToId[str] = id;
					History.idToState[id] = newState;
				}
			}

			// Return ID
			return id;
		};

		/**
		* History.normalizeState(State)
		* Expands a State Object
		* @param {object} State
		* @return {object}
		*/
		History.normalizeState = function (oldState) {
			// Prepare
			if (!oldState || (typeof oldState !== 'object')) {
				oldState = {};
			}

			// Check
			if (typeof oldState.normalized !== 'undefined') {
				return oldState;
			}

			// Adjust
			if (!oldState.data || (typeof oldState.data !== 'object')) {
				oldState.data = {};
			}

			// ----------------------------------------------------------------------

			// Create
			var newState = {};
			newState.normalized = true;
			newState.title = oldState.title || '';

			newState.url = History.getFullUrl((oldState.url || document.location.href));

			newState.hash = History.getShortUrl(newState.url);
			newState.data = History.cloneObject(oldState.data);

			// Fetch ID
			newState.id = History.getIdByState(newState);

			// ----------------------------------------------------------------------

			// Clean the URL
			newState.cleanUrl = newState.url.replace(/\??\&_suid.*/, '');
			newState.url = newState.cleanUrl;

			// Check to see if we have more than just a url
			var dataNotEmpty = !History.isEmptyObject(newState.data);

			// Apply
			if (newState.title || dataNotEmpty) {
				// Add ID to Hash
				newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/, '');
				if (!/\?/.test(newState.hash)) {
					newState.hash += '?';
				}
				newState.hash += '&_suid=' + newState.id;
			}

			// Create the Hashed URL
			newState.hashedUrl = History.getFullUrl(newState.hash);

			// ----------------------------------------------------------------------

			// Update the URL if we have a duplicate
			if ((History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState)) {
				newState.url = newState.hashedUrl;
			}

			// ----------------------------------------------------------------------

			// Return
			return newState;
		};

		/**
		* History.createStateObject(data,title,url)
		* Creates a object based on the data, title and url state params
		* @param {object} data
		* @param {string} title
		* @param {string} url
		* @return {object}
		*/
		History.createStateObject = function (data, title, url) {
			// Hashify
			var State = {
				'data': data,
				'title': title,
				'url': url
			};

			// Expand the State
			State = History.normalizeState(State);

			// Return object
			return State;
		};

		/**
		* History.getStateById(id)
		* Get a state by it's UID
		* @param {String} id
		*/
		History.getStateById = function (id) {
			// Prepare
			id = String(id);

			// Retrieve
			var State = History.idToState[id] || History.store.idToState[id] || undefined;

			// Return State
			return State;
		};

		/**
		* Get a State's String
		* @param {State} passedState
		*/
		History.getStateString = function (passedState) {
			// Prepare
			var State = History.normalizeState(passedState);

			// Clean
			var cleanedState = {
				data: State.data,
				title: passedState.title,
				url: passedState.url
			};

			// Fetch
			var str = JSON.stringify(cleanedState);

			// Return
			return str;
		};

		/**
		* Get a State's ID
		* @param {State} passedState
		* @return {String} id
		*/
		History.getStateId = function (passedState) {
			// Prepare
			var State = History.normalizeState(passedState);

			// Fetch
			var id = State.id;

			// Return
			return id;
		};

		/**
		* History.getHashByState(State)
		* Creates a Hash for the State Object
		* @param {State} passedState
		* @return {String} hash
		*/
		History.getHashByState = function (passedState) {
			// Prepare
			var hash, State = History.normalizeState(passedState);

			// Fetch
			hash = State.hash;

			// Return
			return hash;
		};

		/**
		* History.extractId(url_or_hash)
		* Get a State ID by it's URL or Hash
		* @param {string} url_or_hash
		* @return {string} id
		*/
		History.extractId = function (url_or_hash) {
			// Prepare
			var id;

			// Extract
			var parts, url;
			parts = /(.*)\&_suid=([0-9]+)$/.exec(url_or_hash);
			url = parts ? (parts[1] || url_or_hash) : url_or_hash;
			id = parts ? String(parts[2] || '') : '';

			// Return
			return id || false;
		};

		/**
		* History.isTraditionalAnchor
		* Checks to see if the url is a traditional anchor or not
		* @param {String} url_or_hash
		* @return {Boolean}
		*/
		History.isTraditionalAnchor = function (url_or_hash) {
			// Check
			var isTraditional = !(/[\/\?\.]/.test(url_or_hash));

			// Return
			return isTraditional;
		};

		/**
		* History.extractState
		* Get a State by it's URL or Hash
		* @param {String} url_or_hash
		* @return {State|null}
		*/
		History.extractState = function (url_or_hash, create) {
			// Prepare
			var State = null;
			create = create || false;

			// Fetch SUID
			var id = History.extractId(url_or_hash);
			if (id) {
				State = History.getStateById(id);
			}

			// Fetch SUID returned no State
			if (!State) {
				// Fetch URL
				var url = History.getFullUrl(url_or_hash);

				// Check URL
				id = History.getIdByUrl(url) || false;
				if (id) {
					State = History.getStateById(id);
				}

				// Create State
				if (!State && create && !History.isTraditionalAnchor(url_or_hash)) {
					State = History.createStateObject(null, null, url);
				}
			}

			// Return
			return State;
		};

		/**
		* History.getIdByUrl()
		* Get a State ID by a State URL
		*/
		History.getIdByUrl = function (url) {
			// Fetch
			var id = History.urlToId[url] || History.store.urlToId[url] || undefined;

			// Return
			return id;
		};

		/**
		* History.getLastSavedState()
		* Get an object containing the data, title and url of the current state
		* @return {Object} State
		*/
		History.getLastSavedState = function () {
			return History.savedStates[History.savedStates.length - 1] || undefined;
		};

		/**
		* History.getLastStoredState()
		* Get an object containing the data, title and url of the current state
		* @return {Object} State
		*/
		History.getLastStoredState = function () {
			return History.storedStates[History.storedStates.length - 1] || undefined;
		};

		/**
		* History.hasUrlDuplicate
		* Checks if a Url will have a url conflict
		* @param {Object} newState
		* @return {Boolean} hasDuplicate
		*/
		History.hasUrlDuplicate = function (newState) {
			// Prepare
			var hasDuplicate = false;

			// Fetch
			var oldState = History.extractState(newState.url);

			// Check
			hasDuplicate = oldState && oldState.id !== newState.id;

			// Return
			return hasDuplicate;
		};

		/**
		* History.storeState
		* Store a State
		* @param {Object} newState
		* @return {Object} newState
		*/
		History.storeState = function (newState) {
			// Store the State
			History.urlToId[newState.url] = newState.id;

			// Push the State
			History.storedStates.push(History.cloneObject(newState));

			// Return newState
			return newState;
		};

		/**
		* History.isLastSavedState(newState)
		* Tests to see if the state is the last state
		* @param {Object} newState
		* @return {boolean} isLast
		*/
		History.isLastSavedState = function (newState) {
			// Prepare
			var isLast = false;

			// Check
			if (History.savedStates.length) {
				var 
					newId = newState.id,
					oldState = History.getLastSavedState(),
					oldId = oldState.id;

				// Check
				isLast = (newId === oldId);
			}

			// Return
			return isLast;
		};

		/**
		* History.saveState
		* Push a State
		* @param {Object} newState
		* @return {boolean} changed
		*/
		History.saveState = function (newState) {
			// Check Hash
			if (History.isLastSavedState(newState)) {
				return false;
			}

			// Push the State
			History.savedStates.push(History.cloneObject(newState));

			// Return true
			return true;
		};

		/**
		* History.getStateByIndex()
		* Gets a state by the index
		* @param {integer} index
		* @return {Object}
		*/
		History.getStateByIndex = function (index) {
			// Prepare
			var State = null;

			// Handle
			if (typeof index === 'undefined') {
				// Get the last inserted
				State = History.savedStates[History.savedStates.length - 1];
			}
			else if (index < 0) {
				// Get from the end
				State = History.savedStates[History.savedStates.length + index];
			}
			else {
				// Get from the beginning
				State = History.savedStates[index];
			}

			// Return State
			return State;
		};

		// ----------------------------------------------------------------------
		// Hash Helpers

		/**
		* History.getHash()
		* Gets the current document hash
		* @return {string}
		*/
		History.getHash = function () {
			var hash = History.unescapeHash(document.location.hash);
			//			var hash = document.location.hash;
			return hash;
		};

		/**
		* History.unescapeString()
		* Unescape a string
		* @param {String} str
		* @return {string}
		*/
		History.unescapeString = function (str) {
			return str;
		};

		/**
		* History.unescapeHash()
		* normalize and Unescape a Hash
		* @param {String} hash
		* @return {string}
		*/
		History.unescapeHash = function (hash) {
			// Prepare
			var result = History.normalizeHash(hash);

			// Return result
			return result;
		};

		/**
		* History.normalizeHash()
		* normalize a hash across browsers
		* @return {string}
		*/
		History.normalizeHash = function (hash) {
			var result = hash.replace(/[^#]*#/, '').replace(/#.*/, '');

			// Return result
			return result;
		};

		/**
		* History.setHash(hash)
		* Sets the document hash
		* @param {string} hash
		* @return {History}
		*/
		History.setHash = function (hash, queue) {
			// Handle Queueing
			if (queue !== false && History.busy()) {
				// Wait + Push to Queue
				//History.debug('History.setHash: we must wait', arguments);
				History.pushQueue({
					scope: History,
					callback: History.setHash,
					args: arguments,
					queue: queue
				});
				return false;
			}

			// Log
			//History.debug('History.setHash: called',hash);

			// Prepare
			var adjustedHash = History.escapeHash(hash);

			// Make Busy + Continue
			History.busy(true);

			// Check if hash is a state
			var State = History.extractState(hash, true);
			if (State && !History.emulated.pushState) {
				// Hash is a state so skip the setHash
				//History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments);

				// PushState
				History.pushState(State.data, State.title, State.url, false);
			}
			else if (document.location.hash !== adjustedHash) {
				// Hash is a proper hash, so apply it

				// Handle browser bugs
				if (History.bugs.setHash) {
					// Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249

					// Fetch the base page
					var pageUrl = History.getPageUrl();

					// Safari hash apply
					History.pushState(null, null, pageUrl + '#' + adjustedHash, false);
				}
				else {
					// Normal hash apply
					document.location.hash = adjustedHash;
				}
			}

			// Chain
			return History;
		};

		/**
		* History.escape()
		* normalize and Escape a Hash
		* @return {string}
		*/
		History.escapeHash = function (hash) {
			var result = History.normalizeHash(hash);

			return result;
		};

		/**
		* History.getHashByUrl(url)
		* Extracts the Hash from a URL
		* @param {string} url
		* @return {string} url
		*/
		History.getHashByUrl = function (url) {
			// Extract the hash
			var hash = String(url)
				.replace(/([^#]*)#?([^#]*)#?(.*)/, '$2')
				;

			// Unescape hash
			hash = History.unescapeHash(hash);

			// Return hash
			return hash;
		};

		/**
		* History.setTitle(title)
		* Applies the title to the document
		* @param {State} newState
		* @return {Boolean}
		*/
		History.setTitle = function (newState) {
			// Prepare
			var title = newState.title;

			// Initial
			if (!title) {
				var firstState = History.getStateByIndex(0);
				if (firstState && firstState.url === newState.url) {
					title = firstState.title || History.options.initialTitle;
				}
			}

			// Apply
			try {
				document.getElementsByTagName('title')[0].innerHTML = title.replace('<', '&lt;').replace('>', '&gt;').replace(' & ', ' &amp; ');
			}
			catch (Exception) { }
			document.title = title;

			// Chain
			return History;
		};

		// ----------------------------------------------------------------------
		// Queueing

		/**
		* History.queues
		* The list of queues to use
		* First In, First Out
		*/
		History.queues = [];

		/**
		* History.busy(value)
		* @param {boolean} value [optional]
		* @return {boolean} busy
		*/
		History.busy = function (value) {
			// Apply
			if (typeof value !== 'undefined') {
				//History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length);
				History.busy.flag = value;
			}
			// Default
			else if (typeof History.busy.flag === 'undefined') {
				History.busy.flag = false;
			}

			// Queue
			if (!History.busy.flag) {
				// Execute the next item in the queue
				clearTimeout(History.busy.timeout);
				var fireNext = function () {
					if (History.busy.flag) return;
					for (var i = History.queues.length - 1; i >= 0; --i) {
						var queue = History.queues[i];
						if (queue.length === 0) continue;
						var item = queue.shift();
						History.fireQueueItem(item);
						History.busy.timeout = setTimeout(fireNext, History.options.busyDelay);
					}
				};
				History.busy.timeout = setTimeout(fireNext, History.options.busyDelay);
			}

			// Return
			return History.busy.flag;
		};

		/**
		* History.fireQueueItem(item)
		* Fire a Queue Item
		* @param {Object} item
		* @return {Mixed} result
		*/
		History.fireQueueItem = function (item) {
			return item.callback.apply(item.scope || History, item.args || []);
		};

		/**
		* History.pushQueue(callback,args)
		* Add an item to the queue
		* @param {Object} item [scope,callback,args,queue]
		*/
		History.pushQueue = function (item) {
			// Prepare the queue
			History.queues[item.queue || 0] = History.queues[item.queue || 0] || [];

			// Add to the queue
			History.queues[item.queue || 0].push(item);

			// Chain
			return History;
		};

		/**
		* History.queue (item,queue), (func,queue), (func), (item)
		* Either firs the item now if not busy, or adds it to the queue
		*/
		History.queue = function (item, queue) {
			// Prepare
			if (typeof item === 'function') {
				item = {
					callback: item
				};
			}
			if (typeof queue !== 'undefined') {
				item.queue = queue;
			}

			// Handle
			if (History.busy()) {
				History.pushQueue(item);
			} else {
				History.fireQueueItem(item);
			}

			// Chain
			return History;
		};

		/**
		* History.clearQueue()
		* Clears the Queue
		*/
		History.clearQueue = function () {
			History.busy.flag = false;
			History.queues = [];
			return History;
		};


		// ----------------------------------------------------------------------
		// IE Bug Fix

		/**
		* History.stateChanged
		* States whether or not the state has changed since the last double check was initialised
		*/
		History.stateChanged = false;

		/**
		* History.doubleChecker
		* Contains the timeout used for the double checks
		*/
		History.doubleChecker = false;

		/**
		* History.doubleCheckComplete()
		* Complete a double check
		* @return {History}
		*/
		History.doubleCheckComplete = function () {
			// Update
			History.stateChanged = true;

			// Clear
			History.doubleCheckClear();

			// Chain
			return History;
		};

		/**
		* History.doubleCheckClear()
		* Clear a double check
		* @return {History}
		*/
		History.doubleCheckClear = function () {
			// Clear
			if (History.doubleChecker) {
				clearTimeout(History.doubleChecker);
				History.doubleChecker = false;
			}

			// Chain
			return History;
		};

		/**
		* History.doubleCheck()
		* Create a double check
		* @return {History}
		*/
		History.doubleCheck = function (tryAgain) {
			// Reset
			History.stateChanged = false;
			History.doubleCheckClear();

			// Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does)
			// Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940
			if (History.bugs.ieDoubleCheck) {
				// Apply Check
				History.doubleChecker = setTimeout(
					function () {
						History.doubleCheckClear();
						if (!History.stateChanged) {
							//History.debug('History.doubleCheck: State has not yet changed, trying again', arguments);
							// Re-Attempt
							tryAgain();
						}
						return true;
					},
					History.options.doubleCheckInterval
				);
			}

			// Chain
			return History;
		};

		// ----------------------------------------------------------------------
		// Safari Bug Fix

		/**
		* History.safariStatePoll()
		* Poll the current state
		* @return {History}
		*/
		History.safariStatePoll = function () {
			// Poll the URL

			// Get the Last State which has the new URL
			var 
				urlState = History.extractState(document.location.href),
				newState;

			// Check for a difference
			if (!History.isLastSavedState(urlState)) {
				newState = urlState;
			}
			else {
				return;
			}

			// Check if we have a state with that url
			// If not create it
			if (!newState) {
				//History.debug('History.safariStatePoll: new');
				newState = History.createStateObject();
			}

			// Apply the New State
			//History.debug('History.safariStatePoll: trigger');
			History.Adapter.trigger(window, 'popstate');

			// Chain
			return History;
		};

		// ----------------------------------------------------------------------
		// State Aliases

		/**
		* History.back(queue)
		* Send the browser history back one item
		* @param {Integer} queue [optional]
		*/
		History.back = function (queue) {
			//History.debug('History.back: called', arguments);

			// Handle Queueing
			if (queue !== false && History.busy()) {
				// Wait + Push to Queue
				//History.debug('History.back: we must wait', arguments);
				History.pushQueue({
					scope: History,
					callback: History.back,
					args: arguments,
					queue: queue
				});
				return false;
			}

			// Make Busy + Continue
			History.busy(true);

			// Fix certain browser bugs that prevent the state from changing
			History.doubleCheck(function () {
				History.back(false);
			});

			// Go back
			history.go(-1);

			// End back closure
			return true;
		};

		/**
		* History.forward(queue)
		* Send the browser history forward one item
		* @param {Integer} queue [optional]
		*/
		History.forward = function (queue) {
			//History.debug('History.forward: called', arguments);

			// Handle Queueing
			if (queue !== false && History.busy()) {
				// Wait + Push to Queue
				//History.debug('History.forward: we must wait', arguments);
				History.pushQueue({
					scope: History,
					callback: History.forward,
					args: arguments,
					queue: queue
				});
				return false;
			}

			// Make Busy + Continue
			History.busy(true);

			// Fix certain browser bugs that prevent the state from changing
			History.doubleCheck(function () {
				History.forward(false);
			});

			// Go forward
			history.go(1);

			// End forward closure
			return true;
		};

		/**
		* History.go(index,queue)
		* Send the browser history back or forward index times
		* @param {Integer} queue [optional]
		*/
		History.go = function (index, queue) {
			//History.debug('History.go: called', arguments);

			// Prepare
			var i;

			// Handle
			if (index > 0) {
				// Forward
				for (i = 1; i <= index; ++i) {
					History.forward(queue);
				}
			}
			else if (index < 0) {
				// Backward
				for (i = -1; i >= index; --i) {
					History.back(queue);
				}
			}
			else {
				throw new Error('History.go: History.go requires a positive or negative integer passed.');
			}

			// Chain
			return History;
		};


		// ----------------------------------------------------------------------
		// Initialise

		/**
		* Create the initial State
		*/
		History.saveState(History.storeState(History.extractState(document.location.href, true)));

		/**
		* Bind for Saving Store
		*/
		if (amplify) {
			History.onUnload = function () {
				// Prepare
				var 
					currentStore = amplify.store('History.store') || {},
					item;

				// Ensure
				currentStore.idToState = currentStore.idToState || {};
				currentStore.urlToId = currentStore.urlToId || {};
				currentStore.stateToId = currentStore.stateToId || {};

				// Sync
				for (item in History.idToState) {
					if (!History.idToState.hasOwnProperty(item)) {
						continue;
					}
					currentStore.idToState[item] = History.idToState[item];
				}
				for (item in History.urlToId) {
					if (!History.urlToId.hasOwnProperty(item)) {
						continue;
					}
					currentStore.urlToId[item] = History.urlToId[item];
				}
				for (item in History.stateToId) {
					if (!History.stateToId.hasOwnProperty(item)) {
						continue;
					}
					currentStore.stateToId[item] = History.stateToId[item];
				}

				// Update
				History.store = currentStore;

				// Store
				amplify.store('History.store', currentStore);
			};
			// For Internet Explorer
			History.intervalList.push(setInterval(History.onUnload, History.options.storeInterval));
			// For Other Browsers
			History.Adapter.bind(window, 'beforeunload', History.onUnload);
			History.Adapter.bind(window, 'unload', History.onUnload);
			// Both are enabled for consistency
		}


		// ----------------------------------------------------------------------
		// HTML5 State Support

		if (History.emulated.pushState) {
			/*
			* Provide Skeleton for HTML4 Browsers
			*/

			// Prepare
			var emptyFunction = function () { };
			History.pushState = History.pushState || emptyFunction;
			History.replaceState = History.replaceState || emptyFunction;
		}
		else {
			/*
			* Use native HTML5 History API Implementation
			*/

			/**
			* History.onPopState(event,extra)
			* Refresh the Current State
			*/
			History.onPopState = function (event) {
				// Reset the double check
				History.doubleCheckComplete();

				// Check for a Hash, and handle apporiatly
				var currentHash = History.getHash();
				if (currentHash) {
					// Expand Hash
					var currentState = History.extractState(currentHash || document.location.href, true);
					if (currentState) {
						// We were able to parse it, it must be a State!
						// Let's forward to replaceState
						//History.debug('History.onPopState: state anchor', currentHash, currentState);
						History.replaceState(currentState.data, currentState.title, currentState.url, false);
					}
					else {
						// Traditional Anchor
						//History.debug('History.onPopState: traditional anchor', currentHash);
						History.Adapter.trigger(window, 'anchorchange');
						History.busy(false);
					}

					// We don't care for hashes
					History.expectedStateId = false;
					return false;
				}

				// Prepare
				var newState = false;

				// Prepare
				event = event || {};
				if (typeof event.state === 'undefined') {
					// jQuery
					if (typeof event.originalEvent !== 'undefined' && typeof event.originalEvent.state !== 'undefined') {
						event.state = event.originalEvent.state || false;
					}
					// MooTools
					else if (typeof event.event !== 'undefined' && typeof event.event.state !== 'undefined') {
						event.state = event.event.state || false;
					}
				}

				// Ensure
				event.state = (event.state || false);

				// Fetch State
				if (event.state) {
					// Vanilla: Back/forward button was used
					newState = History.getStateById(event.state);
				}
				else if (History.expectedStateId) {
					// Vanilla: A new state was pushed, and popstate was called manually
					newState = History.getStateById(History.expectedStateId);
				}
				else {
					// Initial State
					newState = History.extractState(document.location.href);
				}

				// The State did not exist in our store
				if (!newState) {
					// Regenerate the State
					newState = History.createStateObject(null, null, document.location.href);
				}

				// Clean
				History.expectedStateId = false;

				// Check if we are the same state
				if (History.isLastSavedState(newState)) {
					// There has been no change (just the page's hash has finally propagated)
					//History.debug('History.onPopState: no change', newState, History.savedStates);
					History.busy(false);
					return false;
				}

				// Store the State
				History.storeState(newState);
				History.saveState(newState);

				// Force update of the title
				History.setTitle(newState);

				// Fire Our Event
				History.Adapter.trigger(window, 'statechange');
				History.busy(false);

				// Return true
				return true;
			};
			History.Adapter.bind(window, 'popstate', History.onPopState);

			/**
			* History.pushState(data,title,url)
			* Add a new State to the history object, become it, and trigger onpopstate
			* We have to trigger for HTML4 compatibility
			* @param {object} data
			* @param {string} title
			* @param {string} url
			* @return {true}
			*/
			History.pushState = function (data, title, url, queue) {
				//History.debug('History.pushState: called', arguments);

				// Check the State
				if (History.getHashByUrl(url) && History.emulated.pushState) {
					throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).');
				}

				// Handle Queueing
				if (queue !== false && History.busy()) {
					// Wait + Push to Queue
					//History.debug('History.pushState: we must wait', arguments);
					History.pushQueue({
						scope: History,
						callback: History.pushState,
						args: arguments,
						queue: queue
					});
					return false;
				}

				// Make Busy + Continue
				History.busy(true);

				// Create the newState
				var newState = History.createStateObject(data, title, url);

				// Check it
				if (History.isLastSavedState(newState)) {
					// Won't be a change
					History.busy(false);
				}
				else {
					// Store the newState
					History.storeState(newState);
					History.expectedStateId = newState.id;

					// Push the newState
					history.pushState(newState.id, newState.title, newState.url);

					// Fire HTML5 Event
					History.Adapter.trigger(window, 'popstate');
				}

				// End pushState closure
				return true;
			};

			/**
			* History.replaceState(data,title,url)
			* Replace the State and trigger onpopstate
			* We have to trigger for HTML4 compatibility
			* @param {object} data
			* @param {string} title
			* @param {string} url
			* @return {true}
			*/
			History.replaceState = function (data, title, url, queue) {
				//History.debug('History.replaceState: called', arguments);

				// Check the State
				if (History.getHashByUrl(url) && History.emulated.pushState) {
					throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).');
				}

				// Handle Queueing
				if (queue !== false && History.busy()) {
					// Wait + Push to Queue
					//History.debug('History.replaceState: we must wait', arguments);
					History.pushQueue({
						scope: History,
						callback: History.replaceState,
						args: arguments,
						queue: queue
					});
					return false;
				}

				// Make Busy + Continue
				History.busy(true);

				// Create the newState
				var newState = History.createStateObject(data, title, url);

				// Check it
				if (History.isLastSavedState(newState)) {
					// Won't be a change
					History.busy(false);
				}
				else {
					// Store the newState
					History.storeState(newState);
					History.expectedStateId = newState.id;

					// Push the newState
					history.replaceState(newState.id, newState.title, newState.url);

					// Fire HTML5 Event
					History.Adapter.trigger(window, 'popstate');
				}

				// End replaceState closure
				return true;
			};

			// Be aware, the following is only for native pushState implementations
			// If you are wanting to include something for all browsers
			// Then include it above this if block

			/**
			* Setup Safari Fix
			*/
			if (History.bugs.safariPoll) {
				History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval));
			}

			/**
			* Ensure Cross Browser Compatibility
			*/
			if (navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName || '') === 'Mozilla') {
				/**
				* Fix Safari HashChange Issue
				*/

				// Setup Alias
				History.Adapter.bind(window, 'hashchange', function () {
					History.Adapter.trigger(window, 'popstate');
				});

				// Initialise Alias
				if (History.getHash()) {
					History.Adapter.onDomLoad(function () {
						History.Adapter.trigger(window, 'hashchange');
					});
				}
			}

		} // !History.emulated.pushState

	}; // History.initCore

	// Try and Initialise History
	History.init();

})(window);

