/**
* 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
		sessionStorage = window.sessionStorage || false, // sessionStorage
		setTimeout = window.setTimeout,
		clearTimeout = window.clearTimeout,
		setInterval = window.setInterval,
		clearInterval = window.clearInterval,
		JSON = window.JSON,
		alert = window.alert,
		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;
            }
        };


        // ====================================================================
        // 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,
				args, arg
				;

            // Write to Console
            if (consoleExists) {
                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) {
                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 and eliminate all references to the original contexts
        * @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,
				pageUrl;

            // Create
            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 = {};

        /**
        * 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.noramlizeStore()
        * Noramlize the store by adding necessary values
        */
        History.normalizeStore = function () {
            History.store.idToState = History.store.idToState || {};
            History.store.urlToId = History.store.urlToId || {};
            History.store.stateToId = History.store.stateToId || {};
        };

        /**
        * 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),
				str;

            if (!id) {
                // Find ID via State String
                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 = (new Date()).getTime() + String(Math.random()).replace(/\D/g, '');
                        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) {
            // Variables
            var newState, dataNotEmpty;

            // 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
            newState = {};
            newState.normalized = true;
            newState.title = oldState.title || '';
            newState.url = History.getFullUrl(History.unescapeString(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
            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, cleanedState, str;

            // Fetch
            State = History.normalizeState(passedState);

            // Clean
            cleanedState = {
                data: State.data,
                title: passedState.title,
                url: passedState.url
            };

            // Fetch
            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, id;

            // Fetch
            State = History.normalizeState(passedState);

            // Fetch
            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 State, hash;

            // Fetch
            State = History.normalizeState(passedState);

            // Hash
            hash = State.hash;

            var indexOfQ = hash.indexOf("?");
            if (indexOfQ >= 0) {
                hash = hash.substr(indexOfQ);
            } else {
                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, parts, url;

            // Extract
            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, id, url;
            create = create || false;

            // Fetch SUID
            id = History.extractId(url_or_hash);
            if (id) {
                State = History.getStateById(id);
            }

            // Fetch SUID returned no State
            if (!State) {
                // Fetch URL
                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,
				oldState;

            // Fetch
            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,
				newId, oldState, oldId;

            // Check
            if (History.savedStates.length) {
                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);
            return hash;
        };

        /**
        * History.unescapeString()
        * Unescape a string
        * @param {String} str
        * @return {string}
        */
        History.unescapeString = function (str) {
            // Prepare
            var result = str,
				tmp;

            var maxRepeat = 4;
            // Unescape hash
            while (maxRepeat > 0) {
                //tmp = window.unescape(result);
                tmp = decodeURIComponent(result);
                if (tmp === result) {
                    break;
                }
                result = tmp;
                maxRepeat--;
            }

            // Return result
            return result;
        };

        /**
        * History.unescapeHash()
        * normalize and Unescape a Hash
        * @param {String} hash
        * @return {string}
        */
        History.unescapeHash = function (hash) {
            // Prepare
            var result = History.normalizeHash(hash);

            // Unescape hash
            result = History.unescapeString(result);

            // Return result
            return result;
        };

        /**
        * History.normalizeHash()
        * normalize a hash across browsers
        * @return {string}
        */
        History.normalizeHash = function (hash) {
            // Prepare
            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) {
            // Prepare
            var adjustedHash, State, pageUrl;

            // 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
            adjustedHash = History.escapeHash(hash);

            // Make Busy + Continue
            History.busy(true);

            // Check if hash is a state
            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
                    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) {
            // Prepare
            var result = History.normalizeHash(hash);

            // Escape hash
            result = window.escape(result);

            // IE6 Escape Bug
            if (!History.bugs.hashEscape) {
                // Restore common parts
                result = result
					.replace(/\%21/g, '!')
					.replace(/\%26/g, '&')
					.replace(/\%3D/g, '=')
					.replace(/\%3F/g, '?');
            }

            // Return result
            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,
				firstState;

            // Initial
            if (!title) {
                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 () {
                    var i, queue, item;
                    if (History.busy.flag) return;
                    for (i = History.queues.length - 1; i >= 0; --i) {
                        queue = History.queues[i];
                        if (queue.length === 0) continue;
                        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.busy.flag
        */
        History.busy.flag = false;

        /**
        * 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;
        };


        // ====================================================================
        // HTML5 State Support

        // Non-Native pushState Implementation
        if (History.emulated.pushState) {
            /*
            * Provide Skeleton for HTML4 Browsers
            */

            // Prepare
            var emptyFunction = function () { };
            History.pushState = History.pushState || emptyFunction;
            History.replaceState = History.replaceState || emptyFunction;
        } // History.emulated.pushState

        // Native pushState Implementation
        else {
            /*
            * Use native HTML5 History API Implementation
            */

            /**
            * History.onPopState(event,extra)
            * Refresh the Current State
            */
            History.onPopState = function (event, extra) {
                // Prepare
                var stateId = false, newState = false, currentHash, currentState;

                // Reset the double check
                History.doubleCheckComplete();

                // Check for a Hash, and handle apporiatly
                currentHash = History.getHash();
                if (currentHash) {
                    // Expand Hash
                    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;
                }

                // Ensure
                stateId = History.Adapter.extractEventData('state', event, extra) || false;

                // Fetch State
                if (stateId) {
                    // Vanilla: Back/forward button was used
                    newState = History.getStateById(stateId);
                }
                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;
            };

        } // !History.emulated.pushState


        // ====================================================================
        // Initialise

        /**
        * Load the Store
        */
        if (sessionStorage) {
            // Fetch
            try {
                History.store = JSON.parse(sessionStorage.getItem('History.store')) || {};
            }
            catch (err) {
                History.store = {};
            }

            // Normalize
            History.normalizeStore();
        }
        else {
            // Default Load
            History.store = {};
            History.normalizeStore();
        }

        /**
        * Clear Intervals on exit to prevent memory leaks
        */
        History.Adapter.bind(window, "beforeunload", History.clearAllIntervals);
        History.Adapter.bind(window, "unload", History.clearAllIntervals);

        /**
        * Create the initial State
        */
        History.saveState(History.storeState(History.extractState(document.location.href, true)));

        /**
        * Bind for Saving Store
        */
        if (sessionStorage) {
            // When the page is closed
            History.onUnload = function () {
                // Prepare
                var currentStore, item;

                // Fetch
                try {
                    currentStore = JSON.parse(sessionStorage.getItem('History.store')) || {};
                }
                catch (err) {
                    currentStore = {};
                }

                // 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;
                History.normalizeStore();

                // Store
                sessionStorage.setItem('History.store', JSON.stringify(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
        }

        // Non-Native pushState Implementation
        if (!History.emulated.pushState) {
            // 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);

