/**
 * History management component that allows you to register arbitrary tokens that signify application
 * history state on navigation actions.  You can then handle the history {@link #change} event in order
 * to reset your application UI to the appropriate state when the user navigates forward or backward through
 * the browser history stack.
 *
 * ## Initializing
 *
 * The {@link #init} method of the History object must be called before using History. This sets up the internal
 * state and must be the first thing called before using History.
 */
Ext.define('Ext.util.History', {
    singleton: true,
    alternateClassName: 'Ext.History',
    mixins: {
        observable: 'Ext.util.Observable'
    },
 
    /**
     * @property
     * True to use `window.top.location.hash` or false to use `window.location.hash`. Must be set before {@link #init} is called
     * because the `hashchange` event listener is added to the window at initialization time.
     */
    useTopWindow: false,
 
    /**
     * @property {Boolean} hashbang If set to `true`, when a hash is set, the hash will be prefixed
     * with an exclamation making it a hash bang instead of just a hash.
     *
     *     Ext.util.History.add('foo'); // will result in #foo
     *
     *     Ext.util.History.hashbang = true;
     *     Ext.util.History.add('bar'); // will result in #!bar
     */
 
    /**
     * @property {String} currentToken The current token.
     * @private
     */
 
    /**
     * @event ready
     * Fires when the Ext.util.History singleton has been initialized and is ready for use.
     * @param {Ext.util.History} history The Ext.util.History singleton.
     */
 
    /**
     * @event change
     * Fires when navigation back or forwards within the local page's history occurs.
     * @param {String} token An identifier associated with the page state at that point in its history.
     */
 
    hashRe: /^(#?!?)/,
 
    constructor: function() {
        var me = this;
 
        me.ready = false;
        me.currentToken = null;
        me.mixins.observable.constructor.call(me);
    },
 
    /**
     * Gets the actual hash from the url. This shouldn't need to be used directly but use the
     * {@link #getToken} method instead.
     *
     * @return {String} The hash from the window object.
     * @private
     */
    getHash: function() {
        return this.win.location.hash.replace(this.hashRe, '');
    },
 
    /**
     * Updates the hash on the window. This shouldn't need to be used directly but use the
     * {@link #add} method instead.
     *
     * @param {String} hash The hash to use
     * @param {Boolean} replace If `true`, the hash passed in will replace the current resource
     * by using the `location.replace()` API.
     * @private
     */
    setHash: function(hash, replace) {
        var me = this,
            hashRe = me.hashRe,
            loc = me.win.location;
 
        // may or may not already be prefixed with # or #! already 
        hash = hash.replace(hashRe, me.hashbang ? '#!' : '#');
 
        try {
            if (replace) {
                loc.replace(hash);
            } else {
                loc.hash = hash;
            }
 
            //need to make sure currentToken is not prefixed 
            me.currentToken = hash.replace(hashRe, '');
        } catch (e) {
            // IE can give Access Denied (esp. in popup windows) 
        }
    },
 
    /**
     * Handles when the hash in the URL has been updated. Will also fired the change event.
     *
     * @param {String} token The token that was changed to
     * @private
     */
    handleStateChange: function(token) {
        // browser won't have # here but may have ! 
        token = token.replace(this.hashRe, '');
 
        this.fireEvent('change', this.currentToken = token);
    },
 
    /**
     * Bootstraps the initialization the location.hash.
     * @private
     */
    startUp: function () {
        var me = this;
 
        me.currentToken = me.getHash();
 
        Ext.get(me.win).on('hashchange', me.onHashChange, me);
 
        me.ready = true;
        me.fireEvent('ready', me);
    },
 
    onHashChange: function () {
        var me = this,
            newHash = me.getHash();
 
        if (newHash !== me.hash) {
            me.hash = newHash;
            me.handleStateChange(newHash);
        }
    },
 
    /**
     * Initializes the global History instance.
     * @param {Function} [onReady] A callback function that will be called once the history
     * component is fully initialized.
     * @param {Object} [scope] The scope (`this` reference) in which the callback is executed.
     * Defaults to the browser window.
     */
    init: function(onReady, scope) {
        var me = this;
 
        if (me.ready) {
            Ext.callback(onReady, scope, [me]);
            return;
        }
 
        if (!Ext.isReady) {
            Ext.onInternalReady(function() {
                me.init(onReady, scope);
            });
            return;
        }
 
        me.win = me.useTopWindow ? window.top : window;
        me.hash = me.getHash();
 
        if (onReady) {
            me.on('ready', onReady, scope, {single: true});
        }
        me.startUp();
    },
 
    /**
     * Add a new token to the history stack. This can be any arbitrary value, although it would
     * commonly be the concatenation of a component id and another id marking the specific history
     * state of that component. Example usage:
     *
     *     // Handle tab changes on a TabPanel
     *     tabPanel.on('tabchange', function(tabPanel, tab){
     *          Ext.History.add(tabPanel.id + ':' + tab.id);
     *     });
     *
     * @param {String} token The value that defines a particular application-specific history state
     * @param {Boolean} [preventDuplicates=true] When true, if the passed token matches the current token
     * it will not save a new history step. Set to false if the same state can be saved more than once
     * at the same history stack location.
     *
     * @return {Boolean} Whether the token was set in the case if the current token matches the token
     * passed.
     */
    add: function(token, preventDuplicates) {
        var me = this,
            set = false;
 
        if (preventDuplicates === false || me.getToken() !== token) {
            me.setHash(token);
            set = true;
        }
 
        return set;
    },
 
    /**
     * Replaces the current resource in history.
     *
     * @param {String} token The value that will replace the current resource in the history state.
     * @param {Boolean} [preventDuplicates=true] When `true`, if the passed token matches the current token
     * it will not save a new history step. Set to `false` if the same state can be saved more than once
     * at the same history stack location.
     *
     * @return {Boolean} Whether the token was set in the case if the current token matches the token
     * passed.
     */
    replace: function(token, preventDuplicates) {
        var me = this,
            set = false;
 
        if (preventDuplicates === false || me.getToken() !== token) {
            this.setHash(token, true);
            set = true;
        }
 
        return set;
    },
 
    /**
     * Programmatically steps back one step in browser history (equivalent to the user pressing the Back button).
     */
    back: function() {
        this.win.history.go(-1);
    },
 
    /**
     * Programmatically steps forward one step in browser history (equivalent to the user pressing the Forward button).
     */
    forward: function(){
        this.win.history.go(1);
    },
 
    /**
     * Retrieves the currently-active history token.
     * @return {String} The token
     */
    getToken: function() {
        return this.ready ? this.currentToken : this.getHash();
    }
});