/**
 * 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();
    }
});