/**
 * Enables reactive actions to handle changes in the hash by using the
 * {@link Ext.route.Mixin#routes routes} configuration in a controller.
 * An example configuration would be:
 *
 *     Ext.define('MyApp.view.main.MainController', {
 *         extend: 'Ext.app.ViewController',
 *         alias: 'controller.app-main',
 *
 *         routes: {
 *             'user/:id': 'onUser'
 *         },
 *
 *         onUser: function (id) {
 *             // ...
 *         }
 *     });
 *
 * The `routes` object can also receive an object to further configure
 * the route, for example you can configure a `before` action that will
 * be executed before the `action` or can cancel the route execution:
 *
 *     Ext.define('MyApp.view.main.MainController', {
 *         extend: 'Ext.app.ViewController',
 *         alias: 'controller.app-main',
 *
 *         routes: {
 *             'user/:id': {
 *                 action: 'onUser',
 *                 before: 'onBeforeUser'
 *             }
 *         },
 *
 *         onBeforeUser: function (id) {
 *             return new Ext.Promise(function (resolve, reject) {
 *                 Ext.Ajax
 *                     .request({
 *                         url: '/check/permission',
 *                         params: {
 *                             route: 'user',
 *                             meta: {
 *                                 id: id
 *                             }
 *                         }
 *                     })
 *                     .then(resolve, reject);
 *             });
 *         },
 *
 *         onUser: function (id) {
 *             // ...
 *         }
 *     });
 */
Ext.define('Ext.route.Route', {
    requires: [
        'Ext.route.Action'
    ],
 
    /**
     * @event beforeroute
     * @member Ext.GlobalEvents
     *
     * Fires when a route is about to be executed. This allows pre-processing to add additional
     * {@link Ext.route.Action#before before} or {@link Ext.route.Action#action action} handlers
     * when the {@link Ext.route.Action Action} is run.
     *
     * The route can be prevented from executing by returning `false` in a listener
     * or executing the {@link Ext.route.Action#stop stop} method on the action.
     *
     * @param {Ext.route.Route} route The route being executed.
     * @param {Ext.route.Action} action The action that will be run.
     */
 
    config: {
        /**
         * @cfg {String} name The name of this route. The name can be used when using
         * {@link Ext.route.Mixin#redirectTo}.
         */
        name: null,
 
        /**
         * @cfg {String} url (required) The url regex to match against.
         */
        url: null,
 
        /**
         * @cfg {Boolean} [allowInactive=false] `true` to allow this route to be triggered on
         * a controller that is not active.
         */
        allowInactive: false,
 
        /**
         * @cfg {Boolean} [caseInsensitive=false] `true` to allow the tokens to be matched with
         * case-insensitive.
         */
        caseInsensitive: false,
 
        /**
         * @private
         * @cfg {Object[]} [handler=[]] The array of connected handlers to this route. Each handler
         * must defined a `scope` and can define an `action`, `before` and/or `exit` handler:
         *
         *     handlers: [{
         *         action: function() {
         *             //...
         *         },
         *         scope: {}
         *     }, {
         *         action: function() {
         *             //...
         *         },
         *         before: function() {
         *             //...
         *         },
         *         scope: {}
         *     }, {
         *         exit: function() {
         *             //...
         *         },
         *         scope: {}
         *     }]
         *
         * The `action`, `before` and `exit` handlers can be a string that will be resolved
         * from the `scope`:
         *
         *     handlers: [{
         *         action: 'onAction',
         *         before: 'onBefore',
         *         exit: 'onExit',
         *         scope: {
         *             onAction: function () {
         *                 //...
         *             },
         *             onBefore: function () {
         *                 //...
         *             },
         *             onExit: function () {
         *                 //...
         *             }
         *         }
         *     }]
         */
        handlers: []
    },
 
    /**
     * @cfg {Object} conditions Optional set of conditions for each token in the url
     * string. Each key should be one of the tokens, each value should be a regex that the
     * token should accept. For example, if you have a Route with a url like
     * `"files/:fileName"` and you want it to match urls like "files/someImage.jpg" then
     * you can set these conditions to allow the :fileName token to accept strings
     * containing a period ("."):
     *
     *     conditions: {
     *         ':fileName': "[0-9a-zA-Z\.]+"
     *     }
     */
 
    /**
     * @property {String} [defaultMatcher='([%a-zA-Z0-9\\-\\_\\s,]+)'] The default RegExp string
     * to use to match parameters with.
     */
    defaultMatcher: '([%a-zA-Z0-9\\-\\_\\s,]+)',
 
    /**
     * @private
     * @property {RegExp} matcherRegex A regular expression to match the token to the configured {@link #url}.
     */
    /**
     * @private
     * @property {RegExp} paramMatchingRegex A regular expression to check if there are parameters in the configured {@link #url}.
     */
    paramMatchingRegex: /:([0-9A-Za-z\_]*)/g,
    /**
     * @private
     * @property {Array} paramsInMatchString An array of parameters in the configured {@link #url}.
     */
 
    /**
     * @protected
     * @property {Boolean} [isRoute=true]
     */
    isRoute: true,
 
    constructor: function (config) {
        var me = this,
            url;
 
        this.initConfig(config);
 
        Ext.apply(me, config, {
            conditions: {}
        });
 
        url = me.getUrl();
 
        me.paramsInMatchString = url.match(me.paramMatchingRegex) || [];
        me.matcherRegex = me.createMatcherRegex(url);
    },
 
    /**
     * Attempts to recognize a given url string and return a meta data object including
     * any URL parameter matches.
     *
     * @param {String} url The url to recognize.
     * @return {Object/Boolean} The matched data, or `false` if no match.
     */
    recognize: function (url) {
        var me = this,
            recognized = me.recognizes(url),
            matches, urlParams;
 
        if (url === me.lastToken) {
            //url matched the lastToken
            return true;
        }
 
        if (recognized) {
            matches = me.matchesFor(url);
            urlParams = url.match(me.matcherRegex);
 
            urlParams.shift();
 
            return Ext.applyIf(matches, {
                historyUrl: url,
                urlParams: urlParams
            });
        }
 
        return false;
    },
 
    /**
     * Returns true if this {@link Ext.route.Route} matches the given url string.
     *
     * @param {String} url The url to test.
     * @return {Boolean} `true` if this {@link Ext.route.Route} recognizes the url.
     */
    recognizes: function (url) {
        return this.matcherRegex.test(url);
    },
 
    /**
     * The method to execute the action using the configured before function which will
     * kick off the actual {@link #actions} on the {@link #controller}.
     *
     * @param token
     * @param {Object} argConfig The object from the {@link Ext.route.Route}'s
     * recognize method call.
     * @return {Ext.promise.Promise}
     */
    execute: function (token, argConfig) {
        var me = this,
            allowInactive = me.getAllowInactive(),
            handlers = me.getHandlers(),
            queue = Ext.route.Router.getQueueRoutes(),
            length = handlers.length,
            befores = [],
            actions = [],
            urlParams = (argConfig && argConfig.urlParams) || [],
            i, handler, scope, action, promises;
 
        me.lastToken = token;
 
        if (!queue) {
            promises = [];
        }
 
        return new Ext.Promise(function (resolve, reject) {
            for (i = 0; i < length; i++) {
                handler = handlers[i];
                scope = handler.scope;
 
                if (!allowInactive && scope.isActive && !scope.isActive()) {
                    continue;
                }
 
                if (queue) {
                    if (handler.before) {
                        befores.push({
                            fn: handler.before,
                            scope: scope
                        });
                    }
 
                    if (handler.action) {
                        actions.push({
                            fn: handler.action,
                            scope: scope
                        });
                    }
                } else {
                    action = {
                        urlParams: urlParams
                    };
 
                    if (handler.before) {
                        action.befores = {
                            fn: handler.before,
                            scope: scope
                        };
                    }
 
                    if (handler.action) {
                        action.actions = {
                            fn: handler.action,
                            scope: scope
                        };
                    }
 
                    action = new Ext.route.Action(action);
 
                    if (Ext.fireEvent('beforeroute', action, me) === false) {
                        action.destroy();
                    } else {
                        promises.push(action.run());
                    }
                }
            }
 
            if (queue) {
                action = new Ext.route.Action({
                    actions: actions,
                    befores: befores,
                    urlParams: urlParams
                });
 
                if (Ext.fireEvent('beforeroute', action, me) === false) {
                    action.destroy();
 
                    reject();
                } else {
                    action.run().then(resolve, reject);
                }
            } else {
                Ext.Promise.all(promises).then(resolve, reject);
            }
        });
    },
 
    /**
     * Returns a hash of matching url segments for the given url.
     *
     * @param {String} url The url to extract matches for
     * @return {Object} matching url segments
     */
    matchesFor: function (url) {
        var params = {},
            keys = this.paramsInMatchString,
            values = url.match(this.matcherRegex),
            length = keys.length,
            i;
 
        //first value is the entire match so reject
        values.shift();
 
        for (i = 0; i < length; i++) {
            params[keys[i].replace(':', '')] = values[i];
        }
 
        return params;
    },
 
    /**
     * Takes the configured url string including wildcards and returns a regex that can be
     * used to match against a url.
     *
     * @param {String} url The url string.
     * @return {RegExp} The matcher regex.
     */
    createMatcherRegex: function (url) {
        // Converts a route string into an array of symbols starting with a colon. e.g.
        // ":controller/:action/:id" => [':controller', ':action', ':id']
        var paramsInMatchString = this.paramsInMatchString,
            conditions = this.conditions,
            defaultMatcher = this.defaultMatcher,
            length = paramsInMatchString.length,
            modifiers = this.getCaseInsensitive() ? 'i' : '',
            i, params, matcher;
 
        if (url === '*') {
            // handle wildcard routes, won't have conditions
            url = url.replace('*', '\\*');
        } else {
            for (i = 0; i < length; i++) {
                params = paramsInMatchString[i];
                matcher = conditions[params] || defaultMatcher;
                url = url.replace(new RegExp(params), matcher);
            }
        }
 
        //we want to match the whole string, so include the anchors
        return new RegExp('^' + url + '$', modifiers);
    },
 
    /*
     * Adds a handler to the {@link #handlers} stack.
     *
     * @param {Object} handler An object to describe the handler. A handler should define a `fn`
     * and `scope`. If the `fn` is a String, the function will be resolved from the `scope`.
     * @return {Ext.route.Route} this
     */
    addHandler: function (handler) {
        var handlers = this.getHandlers();
 
        handlers.push(handler);
 
        return this;
    },
 
    /**
     * Removes a handler from th {@link #handlers} stack. This normally happens when
     * destroying a class instance.
     *
     * @param {Object/Ext.Base} scope The class instance to match handlers with.
     * @param {Object} config An optional object to describe which handlers to remove.
     * This can be used to remove all handlers with a certain `before` or `action`.
     * @return {Ext.route.Route} this
     */
    removeHandler: function (scope, config) {
        var handlers = this.getHandlers(),
            length = handlers.length,
            newHandlers = [],
            i, handler;
 
        for (i = 0; i < length; i++) {
            handler = handlers[i];
 
            if (handler.scope === scope) {
                if (!config ||
                    (
                        Ext.isDefined(config.action) ? handler.action === config.action : true &&
                        Ext.isDefined(config.before) ? handler.before === config.before : true
                    )
                ) {
                    continue;
                }
            }
 
            newHandlers.push(handler);
        }
 
        this.setHandlers(newHandlers);
 
        return this;
    }
});