/**
 * A mixin to allow any class to configure and listen to routes and also change the hash.
 */
Ext.define('Ext.route.Mixin', {
    extend: 'Ext.Mixin',
 
    requires: [
        'Ext.route.Handler',
        'Ext.route.Router'
    ],
 
    mixinConfig: {
        id: 'routerable',
        before: {
            destroy: 'destroyRouterable'
        }
    },
 
    config: {
        /**
         * @cfg {Object} routes
         * @accessor
         *
         * An object of routes to handle hash changes. A route can be defined in a simple way:
         *
         *     routes: {
         *         'foo/bar': 'handleFoo',
         *         'user/:id': 'showUser'
         *     }
         *
         * Where the property is the hash (which can accept a parameter defined by a colon)
         * and the value is the method on the controller to execute. The parameters will get sent
         * in the action method.
         *
         * If no routes match a given hash, an {@link Ext.GlobalEvents#unmatchedroute} event
         * will be fired. This can be listened to in four ways:
         *
         *     Ext.on('unmatchedroute', function(token) {});
         *
         *     Ext.define('MyApp.controller.Foo', {
         *         extend: 'Ext.app.Controller',
         *
         *         listen: {
         *             global: {
         *                 unmatchedroute: 'onUnmatchedRoute'
         *             }
         *         },
         *
         *         onUnmatchedRoute: function(token) {} 
         *     });
         *
         *     Ext.application({
         *         name: 'MyApp',
         *
         *         listen: {
         *             global: {
         *                 unmatchedroute: 'onUnmatchedRoute'
         *             }
         *         },
         *
         *         onUnmatchedRoute: function(token) {} 
         *     });
         *
         *     Ext.application({
         *         name: 'MyApp',
         *
         *         listeners: {
         *             unmatchedroute: 'onUnmatchedRoute'
         *         },
         *
         *         onUnmatchedRoute: function(token) {} 
         *     });
         *
         * There is also a complex means of defining a route where you can use a before action
         * and even specify your own RegEx for the parameter:
         *
         *     routes: {
         *         'foo/bar': {
         *             action: 'handleFoo',
         *             before: 'beforeHandleFoo'
         *         },
         *         'user/:id': {
         *             action: 'showUser',
         *             before: 'beforeShowUser',
         *             conditions: {
         *                 ':id': '([0-9]+)'
         *             }
         *         }
         *     }
         *
         * This will only match if the `id` parameter is a number.
         *
         * The before action allows you to cancel an action. Every before action will get passed
         * an `action` argument with a `resume` and `stop` methods as the last argument of the
         * method and you *MUST* execute either method:
         *
         *     beforeHandleFoo: function (action) {
         *         // some logic here
         *
         *         // this will allow the handleFoo action to be executed
         *         action.resume();
         *     },
         *     handleFoo: function () {
         *         // will get executed due to true being passed in callback in beforeHandleFoo
         *     },
         *     beforeShowUser: function (id, action) {
         *         // allows for async process like an Ajax
         *         Ext.Ajax.request({
         *             url: 'foo.php',
         *             success: function () {
         *                 // will not allow the showUser method to be executed
         *                 // but will continue other queued actions.
         *                 action.stop();
         *             },
         *             failure: function () {
         *                 // will not allow the showUser method to be executed
         *                 // and will not allow other queued actions to be executed.
         *                 action.stop(true);
         *             }
         *         });
         *     },
         *     showUser: function (id) {
         *         // will not get executed due to false being passed in callback in beforeShowUser
         *     }
         *
         * You **MUST** execute the `{@link Ext.route.Action#resume resume}` or
         * `{@link Ext.route.Action#stop stop}` method on the `action` argument. Executing
         * `action.resume();` will continue the action, `action.stop();` will prevent
         * further execution.
         *
         * The default RegEx that will be used is `([%a-zA-Z0-9\\-\\_\\s,]+)` but you can specify
         * any that may suit what you need to accomplish. An example of an advanced condition
         * may be to make a parameter optional and case-insensitive:
         *
         *     routes: {
         *         'user:id': {
         *             action: 'showUser',
         *             before: 'beforeShowUser',
         *             conditions: {
         *                 ':id': '(?:(?:\/){1}([%a-z0-9_,\s\-]+))?'
         *             }
         *         }
         *     }
         *
         * Each route can be named; this allows for the route to be looked up by name instead of
         * url. By default, the route's name will be the url you configure but you can provide
         * the `{@link Ext.route.Route#name name}` config to override the default:
         *
         *     routes: {
         *         'user:id': {
         *             action: 'showUser',
         *             before: 'beforeShowUser',
         *             name: 'user',
         *             conditions: {
         *                 ':id': '(?:(?:\/){1}([%a-z0-9_,\s\-]+))?'
         *             }
         *         }
         *     }
         *
         * The `user:id` route can not be looked up via the `user` name which is useful when using
         * `{@link #redirectTo}`.
         *
         * A wildcard route can also be defined which works exactly like any other route but will
         * always execute before any other route.  To specify a wildcard route, use the `*`
         * as the url:
         *
         *     routes: {
         *         '*': 'onToken'
         *     }
         *
         * Since a wildcard route will execute before any other route, it can delay the execution
         * of other routes allowing for such things like a user session to be retrieved:
         *
         *     routes: {
         *         '*': {
         *             before: 'onBeforeToken'
         *         }
         *     },
         *
         *     onBeforeToken: function () {
         *         return Ext.Ajax.request({
         *             url: '/user/session'
         *         });
         *     }
         *
         * In the above example, no other route will execute unless that
         * {@link Ext.Ajax#request request} returns successfully.
         *
         * You can also use a wildcard route if you need to defer routes until a store has been
         * loaded when an application first starts up:
         *
         *     routes: {
         *         '*': {
         *             before: 'onBeforeToken'
         *         }
         *     },
         *
         *     onBeforeToken: function (action) {
         *         var store = Ext.getStore('Settings');
         *
         *         if (store.loaded) {
         *             action.resume();
         *         } else {
         *             store.on('load', action.resume, action, { single: true });
         *         }
         *     }
         *
         * The valid options are configurations from {@link Ext.route.Handler} and
         * {@link Ext.route.Route}.
         */
        routes: null
    },
 
    destroyRouterable: function() {
        Ext.route.Router.disconnect(this);
    },
 
    applyRoutes: function(routes, oldRoutes) {
        var Router = Ext.route.Router,
            url;
 
        if (routes) {
            for (url in routes) {
                routes[url] = Router.connect(url, routes[url], this);
            }
        }
 
        if (oldRoutes) {
            for (url in oldRoutes) {
                Router.disconnect(this, oldRoutes[url]);
            }
        }
 
        return routes;
    },
 
    /**
     * Update the hash. By default, it will not execute the routes if the current token and the
     * token passed are the same.
     *
     * @param {String/Number/Object/Ext.data.Model} hash The hash to redirect to. The hash can be
     * of several values:
     *  - **String** The hash to exactly be set to.
     *  - **Number** If `1` is passed, {@link Ext.util.History#forward forward} function will be
     * executed. If `-1` is passed, {@link Ext.util.History#bck back} function will be executed.
     *  - **Ext.data.Model** If a model instance is passed, the Model's
     * {@link Ext.data.Model#toUrl toUrl} function will be executed to convert it into a String
     * value to set the hash to.
     *  - **Object** An Object can be passed to control individual tokens in the full hash.
     * The key should be an associated {@link Ext.route.Route Route}'s
     * {@link Ext.route.Route#name name} and the value should be the value of that token
     * in the complete hash. For example, if you have two routes configured, each token in the
     * hash that can be matched for each route can be individually controlled:
     *
     *     routes: {
     *         'foo/bar': 'onFooBar',
     *         'baz/:id': {
     *             action: 'onBaz',
     *             name: 'baz'
     *         }
     *     }
     *
     * If you pass in a hash of `#foo/bar|baz/1`, each route will execute in response. If you want
     * to change only the `baz` route but leave the `foo/bar` route in the hash, you can pass only
     * the `baz` key in an object:
     *
     *     this.redirectTo({
     *         baz : 'baz/5'
     *     });
     *
     * and the resulting hash will be `#foo/bar/|baz/5` and only the `baz` route will execute
     * in reaction but the `foo/bar` will not react since it's associated token in the hash
     * remained the same. If you wanted to update the `baz` route and remove `foo/bar`
     * from the hash, you can set the value to `null`:
     *
     *     this.redirectTo({
     *         'foo/bar': null,
     *         baz: 'baz/3'
     *     });
     *
     * and the resulting hash will be `#baz/3`. Like before, the `baz` route will execute
     * in reaction.
     *
     * @param {Object} opt An optional `Object` describing how to enact the hash being passed in.
     * Valid options are:
     *
     *  - `force` Even if the hash will not change, setting this to `true` will force the
     * {@link Ext.route.Router Router} to react.
     *  - `replace` When set to `true`, this will replace the current resource in the history stack
     * with the hash being set.
     *
     * For backwards compatibility, if `true` is passed instead of an `Object`, this will set
     * the `force` option to `true`.
     *
     * @return {Boolean} Will return `true` if the token was updated.
     */
    redirectTo: function(hash, opt) {
        var currentHash = Ext.util.History.getToken(),
            Router = Ext.route.Router,
            delimiter = Router.getMultipleToken(),
            tokens = currentHash ? currentHash.split(delimiter) : [],
            length = tokens.length,
            force, i, name, obj, route, token, match;
 
        if (hash === -1) {
            return Ext.util.History.back();
        }
        else if (hash === 1) {
            return Ext.util.History.forward();
        }
        else if (hash.isModel) {
            hash = hash.toUrl();
        }
        else if (Ext.isObject(hash)) {
            // Passing an object attempts to replace a token in the hash.
            for (name in hash) {
                obj = hash[name];
 
                if (!Ext.isObject(obj)) {
                    obj = {
                        token: obj
                    };
                }
 
                if (length) {
                    route = Router.getByName(name);
 
                    if (route) {
                        match = false;
 
                        for (= 0; i < length; i++) {
                            token = tokens[i];
 
                            if (route.matcherRegex.test(token)) {
                                match = true;
 
                                if (obj.token) {
                                    // a token was found in the hash, replace it
                                    if (obj.fn && obj.fn.call(this, token, tokens, obj) === false) {
                                        // if the fn returned false, skip update
                                        continue;
                                    }
 
                                    tokens[i] = obj.token;
 
                                    if (obj.force) {
                                        // clear lastToken to force recognition
                                        route.lastToken = null;
                                    }
                                }
                                else {
                                    // remove token
                                    tokens.splice(i, 1);
 
                                    i--;
                                    length--;
 
                                    // reset lastToken
                                    route.lastToken = null;
                                }
                            }
                        }
 
                        if (obj && obj.token && !match) {
                            // a token was not found in the hash, push to the end
                            tokens.push(obj.token);
                        }
                    }
                }
                else if (obj && obj.token) {
                    // there is no current hash, push to the end
                    tokens.push(obj.token);
                }
            }
 
            hash = tokens.join(delimiter);
        }
 
        if (opt === true) {
            // for backwards compatibility
            force = opt;
            opt = null;
        }
        else if (opt) {
            force = opt.force;
        }
 
        length = tokens.length;
 
        if (force && length) {
            for (= 0; i < length; i++) {
                token = tokens[i];
 
                Router.clearLastTokens(token);
            }
        }
 
        if (currentHash === hash) {
            if (force) {
                // hash won't change, trigger handling anyway
                Router.onStateChange(hash);
            }
 
            // hash isn't going to change, return false
            return false;
        }
 
        if (opt && opt.replace) {
            Ext.util.History.replace(hash);
        }
        else {
            Ext.util.History.add(hash);
        }
 
        return true;
    },
 
    privates: {
        afterClassMixedIn: function(targetClass) {
            var proto = targetClass.prototype,
                routes = proto.routes;
 
            if (routes) {
                delete proto.routes;
 
                targetClass.getConfigurator().add({
                    routes: routes
                });
            }
        }
    }
});