(function($) {

    // Initial Setup
    // -------------

    var root = this,
        // The top-level namespace
        activities;

    if (typeof exports !== 'undefined') {
        activities = exports;
    } else {
        activities = root.activities = {};
    }

    // Current version of the library.
    activities.VERSION = '0.3.1';

    // Require jquery.
    if (!$ && (typeof require !== 'undefined')) {
         root.jQuery = $ = require('jquery');
    }

    // Require Backbone.
    if (!root.Backbone && (typeof require !== 'undefined')) {
        root.Backbone = require('backbone');
    }

    // Require Underscore.
    if (!root._ && (typeof require !== 'undefined')) {
        root._ = require('underscore')._;
    }

    root.Backbone.activities = activities;

// Helpers
// -------
activities.helpers = {};

var getPlaces = function(ActivityClass) {
    var place = ActivityClass.prototype.place;

    if (!place) {
        throw new Error("Activity must have a `place` property");
    }

    if (_.isArray(place)) {
        return place;
    }
    else {
        return [ place ];
    }
};

var isArray = function(obj) {
    return _.isArray(obj);
}

activities.helpers.extend = Backbone.View.extend;
activities.helpers.getPlaces = getPlaces;
// jQuery's `$.when` method treates any non deferred objects that it's passed
// as a resolved deferred.
activities.helpers.resolvedPromise = null;

// Event Bus
// ---------
//
// Global eventBus used to communicate between modules.
var eventBus = {};

_.extend(eventBus, Backbone.Events);

activities.getEventBus = function() {
    return eventBus;
};

// activities.History
// ------------------

// Wrapper for Backbone.History, keeps it as it is only overriding the
// `loadUrl` method to trigger the 'historyChange' event.
function History() {
    return Backbone.History.apply(this, arguments);
}

_.extend(History.prototype, Backbone.History.prototype, {

    eventBus: eventBus,

    loadUrl: function(fragmentOverride) {
        var ret = Backbone.History.prototype.loadUrl.apply(this, arguments);
        var path = this.getFragment();

        this.eventBus.trigger("historyChange", path);

        return true;
    }

});

activities.History = History;
activities.history || (activities.history = new History());

// Place Controller
// ----------------
var placeController = {

    eventBus: activities.getEventBus(),
   
    goTo: function(place) {
        this.eventBus.trigger("placeChangeRequest", place);
    }

};

activities.getPlaceController = function() {
    return placeController;
}

/**
 * Initialize `Route` with the given `path` and `options`.
 *
 * Options:
 *
 *   - `sensitive`    enable case-sensitive routes
 *   - `strict`       enable strict matching for trailing slashes
 *
 * @param {String} path
 * @param {Object} options.
 */

function Route(path, options) {
  options = options || {};
  this.path = path;
  this.params = {};
  this.regexp = this.pathRegexp(path
    , this.keys = []
    , options.sensitive
    , options.strict);
}

/**
 * Check if this route matches `path`, if so
 * populate `.params`.
 *
 * @param {String} path
 * @return {Boolean}
 */

Route.prototype.test = function(path){
  var keys = this.keys
    , params = this.params = []
    , m = this.regexp.exec(path);

  if (!m) return false;

  for (var i = 1, len = m.length; i < len; ++i) {
    var key = keys[i - 1];

    var val = 'string' == typeof m[i]
      ? decodeURIComponent(m[i])
      : m[i];

    if (key) {
      params[key.name] = val;
    } else {
      params.push(val);
    }
  }

  return true;
};


Route.prototype.extractParameters = function(path) {
    return this.params;
};

Route.prototype.pathRegexp = function(path, keys, sensitive, strict) {
  if (path instanceof RegExp) return path;
  if (Array.isArray(path)) path = '(' + path.join('|') + ')';
  path = path
    .concat(strict ? '' : '/?')
    .replace(/\/\(/g, '(?:/')
    .replace(/\+/g, '__plus__')
    .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional){
      keys.push({ name: key, optional: !! optional });
      slash = slash || '';
      return ''
        + (optional ? '' : slash)
        + '(?:'
        + (optional ? slash : '')
        + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')'
        + (optional || '');
    })
    .replace(/([\/.])/g, '\\$1')
    .replace(/__plus__/g, '(.+)')
    .replace(/\*/g, '(.*)');
  return new RegExp('^' + path + '$', sensitive ? '' : 'i');
}

activities.Route = Route;

// activities.Place
// ----------------

function Place(params) {
    this.params = params || {};

    this.initialize.apply(this, arguments);
}

_.extend(Place.prototype, {
    pattern: null,

    initialize: function() {},

    // Builds a route from our `string` pattern and params `object`.
    getRoute: function() {
        var p, path = this.pattern;

        for (p in this.params) {
            path = path.replace(":" + p, this.params[p]);
        }

        return path;
    },

    getParams: function() {
        return this.params;
    },

    equals: function(place) {
        if (place instanceof this.constructor && this._equalParams(place)) {
            return true;
        }

        return false;
    },

    _equalParams: function(place) {
        var params = place.getParams();

        return _.isEqual(params, this.params);
    }
});

Place.extend = activities.helpers.extend;
activities.Place = Place;

// activities.Activity
// -------------------
function Activity(place) {
    this.currentPlace = place;

    this.initialize.apply(this, arguments);
}

_.extend(Activity.prototype, {

    placeController: activities.getPlaceController(),
    eventBus: activities.getEventBus(),
                          
    // override
    initialize: function(place) {},

    // override
    start: function(panel) {},

    // override
    stop: function() {},

    // override
    cancel: function() {},

    // override
    mayStop: function() {
        return true;
    },

    goTo: function(place) {
        this.placeController.goTo(place);
    }
});

Activity.extend = activities.helpers.extend;
activities.Activity = Activity;
// activities.Match
// ----------------

// Relates an ´Activity´ subclass to a ´Place´ subclass.
function Match(Place, Activity) {
    this.Place = Place;
    this.Activity = Activity;

    // every place must have a ´pattern´
    this.pattern = Place.prototype.pattern;
}

_.extend(Match.prototype, {

    // Tests a if a place is instance of our ´Place´ class.
    test: function(place) {
        var route, params;

        if (place instanceof activities.Place) {
            if(place instanceof this.Place) {
                return true;
            } else {
                return false;
            }
        }
        
        this.route = new activities.Route(this.pattern);

        return this.route.test(place);
    },

    // Builds a place from a `string` path.
    buildPlace: function(path) {
        var params;

        if (!this.route) {
            throw new Error("place can only be built when the original place is a string");
        }

        params = this.route.extractParameters(path);
        return new this.Place(params);
    }

});

activities.Match = Match;

// activities.DisplayRegion
// ------------------------
var DisplayRegion = function(el) {
    this.setElement(el);
}

_.extend(DisplayRegion.prototype, {

    display: function(view) {
        this.close();

        // Test if it's a Backbone view.
        if (view instanceof Backbone.View) {
            // first render the Backbone view.
            view.render();

            // Insert the rendered view into de DOM.
            this.$el.html(view.el); 
        } else if (view instanceof $ || typeof view === "string") {
            this.$el.html(view);
        } else {
            throw new TypeError("DisplayRegion#show: invalid type for `view`.");
        }
    },

    close: function() {
        this.$el.empty();
    },

    show: function() {
        this.$el.show();
    },

    hide: function() {
        this.$el.hide();
    },

    setElement: function(element) {
        this.$el = $(element);
        this.el = this.$el[0];

        return this;
    }
});

DisplayRegion.extend = activities.helpers.extend;
activities.DisplayRegion = DisplayRegion;

// activities.ActivityManager
// --------------------------
function ActivityManager(displayRegion) {
    this._displayRegion = displayRegion;
    this._matchs = [];
}

_.extend(ActivityManager.prototype, {

    eventBus: activities.getEventBus(),

    // Registers activities to this `ActivityManager`. It can receive a single
    // `Activity` or an array of them.
    register: function(ActivityClass) {
        var i=0, len;

        // If we received an array of activities, register each one.
        if (isArray(ActivityClass)) {
            len = ActivityClass.length;

            for (; i<len; i++) {
                this._register(ActivityClass[i]);
            }

            return;
        }
        
        // We received a single `Activity` to register if we've got to this
        // step.
        return this._register(ActivityClass);
    },

    // Registers a single `Activity` to this `ActivityManager`.
    _register: function(ActivityClass) {
        var i=0, len, PlaceClass, match, placeClasses;

        // We get the activity's place or places.
        placeClasses = getPlaces(ActivityClass);

        len = placeClasses.length;

        // Registering a match between an ´Activity´ and a ´Place´.
        for (; i<len; i++) {
            PlaceClass = placeClasses[i];
            match = new activities.Match(PlaceClass, ActivityClass);

            this._matchs.push(match);
        }
    },

    // For a given `Place` instance, try to find an activity match.
    _findMatch: function(place) {
        var match, _i, _len, _ref;

        _ref = this._matchs;
        for (_i = 0, _len = _ref.length; _i < _len; _i++) {
            match = _ref[_i];

            if (match.test(place)) {
                return match;
            }
        }

        // No match found.
        return false;
    },

    _createPlace: function(path) {
        var match = this._findMatch(path);

        if (match instanceof activities.Match) {
            return match.buildPlace(path);
        }

        // No match found.
        return false;
    },

    reset: function() {
        if (this._displayRegion) {                                                       
            this._displayRegion.close();                                         
            this._displayRegion.hide();                                          
        }                                                                        
        this._deactivateCurrentActivity();                                       
        this._currentActivity = null;                                            
        this._currentPlace = null; 
    },

    // Loads an activity from a place, returns a promise that indicates when
    // the display region had it's content loaded.
    load: function(place) {
        var activity, mayStopNext = true, match,
            // Helper used to respect the method's interface.
            resolvedPromise = activities.helpers.resolvedPromise;

        // Try to find an activity to match the current place.
        match = this._findMatch(place);

        // No match was found so we reset the activity manager.
        if (!match) {
            this.reset();
            this._displayRegion.hide();
            return resolvedPromise;
        }

        this._displayRegion.show();
        
        // If the new place equals the last one, no activity is loaded.
        if (place.equals(this._currentPlace)) {
            return resolvedPromise;
        }

        // Store the current valid `Place` instance.
        this._currentPlace = place;

        // Create a new activity for the current place.
        activity = this._createActivity(match.Activity, place);

        // Perform some deactivation tasks over the previous activity.
        this._deactivateCurrentActivity();

        // Store the new activity as the current one.
        this._currentActivity = activity;
        
        // Perform some activation tasks for the new activity 
        return this._activate(activity);
    },

    _createActivity: function(ActivityClass, place) {
        return new ActivityClass(place);
    },

    _activate: function(activity) {
        var _self = this, 
            // Object that will recieve the activity's callback.
            protectedDisplay = new ProtectedDisplay(this);

        // When the activity loads a view within the display region the promise
        // will be resolved.
        this._promise = protectedDisplay.getPromise();

        // Set a flag indicating that an activity is in the process of being
        // started.
        this._startingNext = true;

        // When the an activity is started we unset this flag.
        $.when(this._promise).then(function() {
            _self._startingNext = false;
        });

        // Starting our new activity.
        activity.start(protectedDisplay);
        
        // returning the promise, so the 'loader' can know when the activity
        // finishes loading.
        return this._promise;
    },

    _deactivateCurrentActivity: function() {
        this._deactivate(this._currentActivity);
    },

    _deactivate: function(activity) {
        var _self = this;

        if (activity) {
            // The current activity is in the process of being started.
            if (this._startingNext) {
                // There's an Activity in the process of being started, so we
                // should cancel it before starting the next activity.
                activity.cancel();
            } else {
                // Stop the running `Activity`.
                activity.stop();
            }
        }
    },

    mayStopCurrentActivity: function() {
        // If the current activity is loading it may be stopped.
        if (!this._currentActivity || this._startingNext) {
            return true;
        }

        // Asking the current activity if it may be stopped.
        return this._currentActivity.mayStop();
    },

    showView: function(view) {
        if (this._displayRegion) {
            this._displayRegion.display(view);
        }
    },

    getCurrentActivity: function() {
        return this._currentActivity;
    },

    getCurrentPlace: function() {
        return this._currentPlace;
    },

    getDisplayRegion: function() {
        return this._displayRegion;
    }

});

var ProtectedDisplay = function(activityManager) {
    // storing the current activity to compare later
    this.activity = activityManager.getCurrentActivity();
    this.activityManager = activityManager;
    this.region = this.activityManager.getDisplayRegion();

    this._deferred = $.Deferred();
    // Promise to inform the activity manager that the activity has finished
    // loading.
    this._promise = this._deferred.promise();
}

_.extend(ProtectedDisplay.prototype, { 
    /**
     * @param view {Backbone.View|String|Element}
     * @param resolve {Boolean} optional, determines if the deferred should be resolved.
     */
    setView: function(view, resolve) {
        var activityManager = this.activityManager;

        if (this.activity == activityManager.getCurrentActivity()) {
            activityManager.showView(view);

            if (typeof resolve === "undefined" || resolve === true) {
                this._deferred.resolve(activityManager._currentPlace);
            }
        }
    },

    getPromise: function() {
        return this._promise;
    },

    finish: function() {
        this._deferred.resolve();
    },

    getRegion: function() {
        return this.activityManager.getDisplayRegion();
    }
});

activities.ActivityManager = ActivityManager;
activities.ProtectedDisplay = ProtectedDisplay;
// activities.Application
// ----------------------
function Application() {
    this._managers = [];

    this._bindEvents();
}

_.extend(Application.prototype, Backbone.Events, {

    eventBus: activities.getEventBus(),

    _bindEvents: function() {
        this.eventBus.bind("placeChangeRequest", this._onPlaceChangeRequest, this);
        this.eventBus.bind("historyChange", this._onHistoryChange, this);
    },

    _unbindEvents: function() {
        this.eventBus.unbind("placeChangeRequest", this._onPlaceChangeRequest);
        this.eventBus.unbind("historyChange", this._onHistoryChange);
    },

    register: function(managers) {
        var i = 0,
            len;
        if (isArray(managers)) {
            len = managers.length;

            for (; i<len; i++) {
                this._register(managers[i]);
            }

        } else {
            this._register(managers);
        }
    },

    _register: function(manager) {
        if(manager instanceof activities.ActivityManager) {
            this._managers.push(manager);
        } else {
            throw new Error("No valid ActivityManager.");
        }
    },

    _mayStop: function() {
        var _i, _len=this._managers.length, manager;

        for (_i=0; _i<_len; _i++) {
            manager = this._managers[_i];

            if (!manager.mayStop()) {
                return false;
            }
        }

        return true;
    },

    _onHistoryChange: function(path) {
        var place = this._createPlace(path);
        
        if (!(place instanceof activities.Place)) {
            this.trigger("placeNotFound", path);
            return;
        }

        this._currentPlace = place;
        
        this._triggerPlaceChange(place);
    },

    _onPlaceChangeRequest: function(place) {

        // Our activity managers didn't let us load a new place.
        if (!this._mayLoadPlace(place)) {
            return;
        }

        this._triggerPlaceChange(place);

        this._navigate(place);
    },

    _mayLoadPlace: function(place) {
        var self = this, _i, _len=this._managers.length, 
            manager;

        // First check if we are trying to load same place
        if(place.equals(this._currentPlace)) {
            return false
        }

        // Then check if we may stop all current activities
        for (_i=0; _i<_len; _i++) {
            manager = this._managers[_i];

            if (!manager.mayStopCurrentActivity()) {
                return false;
            }
        }

        return true;
    },

    _triggerPlaceChange: function(place) {
        var self = this;

        this.trigger("beforePlaceChange", place);

        this._loadManagers(place, function() {
            self.trigger("placeChange", place);
        });
    },

    _loadManagers: function(place, callback) {
        var _i, _len=this._managers.length, 
            manager, promise, promises=[];

        // Try to load an activity in each activity manager
        for (_i=0; _i<_len; _i++) {
            manager = this._managers[_i];
            promise = manager.load(place);
            promises.push(promise);
        }

        // When all managers loaded their activities we invoke the callback
        // function.
        $.when.apply(null, promises).then(callback);
    },

    _navigate: function(place) {
        this._currentPlace = place;

        // Trigger a url change (without triggerring the `route` event).
        activities.history.navigate(place.getRoute(), { navigate: false });
    },

    getCurrentPlace: function() {
        return this._currentPlace;
    },

    _createPlace: function(path) {
        var _i, _len=this._managers.length, 
            manager, place;
                   
        for (_i=0; _i<_len; _i++) {
            manager = this._managers[_i];
            place = manager._createPlace(path);

            if (place) {
                break;
            }
        }

        return place;
    }
});

activities.Application = Application;
}).call(this, this.jQuery || this.Zepto);