/**
 * Represents a mapping between a url and a controller/action pair. May also contain
 * additional params.
 *
 * This is a private internal class that should not need to be used by end-developer code.
 * Its API and existence are subject to change so use at your own risk.
 *
 * @private
 */
Ext.define('Ext.app.route.Route', {
    /**
     * @cfg {String} action The name of the action that will be called on the
     * {@link #controller} if this route is matched.
     */
    action: null,

    /**
     * @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\.]+"
     *     }
     */
    conditions: null,

    /**
     * @cfg {String} controller The name of the Controller whose {@link #action} will be
     * called if this route is matched.
     */
    controller: null,
    
    /**
     * @cfg {Boolean} allowInactive `true` to allow this route to be triggered on
     * a controller that is not active.
     */
    allowInactive: false,

    /**
     * @cfg {String} url (required) The url regex to match against.
     */
    url: null,

    /**
     * @cfg {Function} before An optional function to use to intercept the {@link #action}
     * to do extra means and possibly stop the execution. An example is if the route is
     * for editing a user and you need to verify the current user has permission you can
     * send an {@link Ext.Ajax} request to a server or some arbitrary code.
     *
     * @param {Function} callback A function that MUST be executed passing in a Boolean
     * value to allow execution of the configured action on this {@link Ext.app.route.Route}.
     *
     * Defaults to `null`
     */
    before: null,

    /**
     * @cfg {Boolean} caseInsensitive `true` to allow the tokens to be matched with
     * case-insensitive. Defaults to `false` which will force case matching.
     */
    caseInsensitive: false,

    /**
     * A regular expression to match the token to the configured {@link #url}.
     *
     * @private
     */
    matcherRegex: null,

    /**
     * A regular expression to check if there are parameters in the configured {@link #url}.
     *
     * @private
     */
    paramMatchingRegex: null,

    /**
     * An array of parameters in the configured {@link #url}.
     *
     * @private
     */
    paramsInMatchString: null,

    constructor : function(config) {
        var me = this,
            url;

        Ext.apply(me, config, {
            conditions : {}
        });

        url = me.url;

        me.paramMatchingRegex  = new RegExp(/:([0-9A-Za-z\_]*)/g);
        me.paramsInMatchString = url.match(me.paramMatchingRegex) || [];
        me.matcherRegex        = me.createMatcherRegex(url);
    },

    /**
     * Attempts to recognize a given url string and return controller/action pair for it.
     *
     * @param {String} url The url to recognize.
     * @return {Object/Boolean} The matched data, or `false` if no match.
     */
    recognize : function(url) {
        var me = this,
            controller = me.controller,
            matches, args;

        if ((me.allowInactive || controller.isActive()) && me.recognizes(url)) {
            //find parameter matches
            matches = me.matchesFor(url);
            //find the arguments for the parameters
            args    = url.match(me.matcherRegex);

            //first one is the entire match, remove
            args.shift();

            return Ext.applyIf(matches, {
                controller : controller,
                action     : me.action,
                historyUrl : url,
                args       : args
            });
        }

        return false;
    },

    /**
     * Returns true if this {@link Ext.app.route.Route} matches the given url string.
     *
     * @private
     * @param {String} url The url to test.
     * @return {Boolean} `true` if this {@link Ext.app.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 #action} on the {@link #controller}.
     *
     * @private
     * @param {String} token The hash to execute with.
     * @param {Object} argConfig The object from the {@link Ext.app.route.Route}'s
     * recognize method call.
     * @param {Function} callback An optional callback function to execute after the
     * {@link #action} is executed.
     * @param {Object} scope The scope to execute the callback with, defaults to this
     * {@link Ext.app.route.Route}.
     */
    execute : function(token, argConfig, callback, scope) {
        var args           = argConfig.args || [],
            before         = this.before,
            controller     = this.controller,
            beforeCallback = this.createCallback(argConfig, callback, scope);

        if (before) {
            args.push(beforeCallback);

            if (Ext.isString(before)) {
                //get method from the controller
                before = this.before = controller[before];
            }

            if (before) {
                before.apply(controller, args);
            }
            //<debug>
            else {
                Ext.log.warn('The before action: ' + this.before +
                    ' was not found on the controller. The action method will not be executed.');
            }
            //</debug>
        } else {
            //If no before was specified, proceed to action
            beforeCallback.resume();
        }
    },

    /**
     * Returns a hash of matching url segments for the given url.
     *
     * @private
     * @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),
            i      = 0,
            len    = keys.length;

        //first value is the entire match so reject
        values.shift();

        for (; i < len; 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.
     *
     * @private
     * @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,
            i                   = 0,
            len                 = paramsInMatchString.length,
            format              = Ext.util.Format.format,
            modifiers           = this.caseInsensitive ? 'i' : '',
            params, cond, matcher;

        for (; i < len; i++) {
            params  = paramsInMatchString[i];
            cond    = conditions[params];
            matcher = format('{0}', cond || '([%a-zA-Z0-9\\-\\_\\s,]+)');

            url = url.replace(new RegExp(params), matcher);
        }

        //we want to match the whole string, so include the anchors
        return new RegExp('^' + url + '$', modifiers);
    },

    /**
     * Creates the callback function to execute in the configured {@link #before} function.
     *
     * @private
     * @param {Object} args The arguments found from the {@link Ext.app.route.Route}'s
     * recognize call.
     * @param {Function} callback The function to be executed after the {@link #action}
     * has been executed.
     * @param {Object} scope The scope to execute on the callback function, defaults to
     * the {@link Ext.app.route.Route}.
     * @return {Object} An object with the `resume` and `stop` methods on it to control to continue
     * with the action or not.
     */
    createCallback : function (args, callback, scope) {
        var me = this;

        scope = scope || me;

        return {
            resume : function() {
                var controller = me.controller,
                    action = me.action,
                    resume;

                if (Ext.isString(action)) {
                    //get method from the controller
                    action = controller[action];
                }

                //get the parameter arguments
                args = args && args.args ? args.args : [];

                //remove the action argument from the before method
                resume = args.pop();

                if (resume && !Ext.isObject(resume)) {
                    args.push(resume);
                }

                //make sure there is an action
                if (action) {
                    me.action = action;

                    //execute the action on the controller scoping to the controller
                    action.apply(controller, args);
                }
                //<debug>
                else {
                    Ext.log.warn('The action: ' + me.action + ' was not found on the controller.');
                }
                //</debug>

                if (callback) {
                    callback.call(scope);
                }
            },

            stop : function(all) {
                if (callback) {
                    callback.call(scope, all);
                }
            }
        };
    }
});
