(function(root, win) {

  // Recklessly stolen from Backbone.js and modified a little
  var namedParam = /:\w+/g,
      splatParam = /\*\w+/g,
      subPath = /\*/g,
      escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g,
      routeToRegExp = function (route) {
        route = route.replace(escapeRegExp, "\\$&").replace(namedParam, "([^/]+)").replace(splatParam, "(.*)?").replace(subPath, ".*?");
        return new RegExp("^" + route + "$");
      },
      normalizePathname = function() {
        var loc = window.location;
        var pathname = loc.pathname + loc.hash;
        return pathname || "/";
      };


  var Pather = (function () {
    var addDOMListener = function(type, listener, useCapture) {
      addDOMListener = window[window.addEventListener ? 'addEventListener' : 'attachEvent'].bind(window);
      return addDOMListener(type, listener, useCapture);
    };

    var listeners = [];

    function find(listener) {
      for (var i = listeners.length; i--;) {
        var l = listeners[i];
        if (l.route === listener.route && l.event === listener.event && l.handler === listener.handler) {
          return l;
        }
      }
      return null;
    }

    function remove(listener) {
      var idx = listeners.indexOf(listener);
      if (idx > -1) {
        listeners.splice(idx, 1);
      }
    }

    function call(listener, params) {
      listener.handler.apply(listener, params);
      if (listener.once) {
        remove(listener);
      }
    }

    function deepEqual(a1, a2) {
      return JSON.stringify(a1) == JSON.stringify(a2);
    }

    function check(listener, path) {
      if (listener.regexp.test(path)) {
        var params = listener.regexp.exec(path).slice(1);
        if (listener.event === 'enter' && !(listener.active && deepEqual(listener.prevParams, params))) {
          // Its a listener for 'enter' and wasn't active on last check, so call the listener function
          call(listener, params);
        }
        // Store for next check
        listener.active = true;
        listener.prevParams = params;
      }
      else {
        if (listener.hasOwnProperty('prevParams') && listener.event === 'leave') {
          // Its a listener for 'leave' and the regexp matched on last check
          call(listener, listener.prevParams);
          delete listener.prevParams;
        }
        listener.active = false;
      }
    }

    /**
     *  A decorator that turns any given funciton into a function that will be called with a listener object
     *  built from the original params
     */
    function withListener(func) {
      return function (route, event, handler) {
        if (arguments.length === 2) {
          handler = event;
          event = 'enter';
        }
        var listener = {
          route: route,
          event: event,
          handler: handler,
          regexp: (route instanceof RegExp) ? route : routeToRegExp(route)
        };
        return func.call(this, listener);
      };
    }

    function Pather(config) {
      config || (config = {});

      if (!(config.hasOwnProperty('pushState') || config.pushState === false)) {
        addDOMListener('popstate', this.checkAll.bind(this))
      }
      if (!(config.hasOwnProperty('hash') || config.hash === false)) {
        addDOMListener('hashchange', this.checkAll.bind(this))
      }
    }

    Pather.prototype.on = withListener(function (listener) {
      listeners.push(listener);
      check(listener, normalizePathname());
    });

    Pather.prototype.once = withListener(function (listener) {
      listener.once = true;
      listeners.push(listener);
      check(listener, normalizePathname());
    });

    Pather.prototype.removeListener = withListener(function (listener) {
      remove(find(listener));
    });

    Pather.prototype.removeAllListeners = function () {
      listeners = [];
    };

    /**
     * Convenience method to check if the given regex or route string matches the current document.location.pathname
     * @param route
     * @returns true or false
     */
    Pather.prototype.matches = function (/* String|RegExp */ route) {
      return !!this.match(route);
    };

    /**
     * Match the given regex or route string against the current document.location.pathname and return an array with the
     * values for each capturing parenthesis. Returns null if there is no match.
     * @param route
     * @returns the values for captured parenthesis
     */
    Pather.prototype.match = function (/* String|RegExp */ route) {
      var regexp = (route instanceof RegExp) ? route : routeToRegExp(route);
      var matches = regexp.exec(normalizePathname());
      return matches && matches.slice(1);
    };

    /**
     * Checks whether any of the registered listeners matches the given path
     * @param pathname
     */
    Pather.prototype.has = function (pathname) {
      return listeners.some(function(listener) {
        return listener.regexp.test(pathname);
      });
    };

    Pather.prototype.checkAll = function () {
      listeners.forEach(function (listener) {
        check(listener, normalizePathname());
      });
    };

    return Pather;

  })();

  // Only have one listener per page
  var instance = new Pather;

  if (typeof exports !== 'undefined') {
    if (typeof module !== 'undefined' && module.exports) {
      module.exports = instance;
    }
  } else {
    win.Pather = instance;
  }

  window[window.addEventListener ? 'addEventListener' : 'attachEvent']( 'popstate', function(e) {
    instance.checkAll();
  }, false);

})(this, window);