/**
 * 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.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 });
         *         }
         *     }
         */
        routes: null
    },
 
    destroyRouterable: function () {
        Ext.route.Router.disconnect(this);
    },
 
    updateRoutes: function(routes, oldRoutes) {
         var me = this,
            Router = Ext.route.Router,
            url, config, method;
 
        if (oldRoutes) {
            for (url in oldRoutes) {
                config = oldRoutes[url];
 
                if (Ext.isString(config)) {
                    config = {
                        action: config,
                        name: url
                    };
                }
 
                Router.disconnect(me, config);
            }
        }
 
        if (routes) {
            for (url in routes) {
                config = routes[url];
 
                if (Ext.isString(config)) {
                    config = {
                        action : config
                    };
                }
 
                method = config.action;
 
                //connect the route config to the Router
                Router.connect(url, config, me);
            }
        }
    },
 
    /**
     * 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. They 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 me          = this,
            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
                });
            }
        }
    }
});