text: "use strict"
  
  var Vnode = require("../render/vnode")
  var Promise = require("../promise/promise")
  
  var buildPathname = require("../pathname/build")
  var parsePathname = require("../pathname/parse")
  var compileTemplate = require("../pathname/compileTemplate")
  var assign = require("../pathname/assign")
  
  var sentinel = {}
  
  module.exports = function($window, mountRedraw) {
  	var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout
  	var supportsPushState = typeof $window.history.pushState === "function"
  	var routePrefix = "#!"
  	var fireAsync
  
  	function setPath(path, data, options) {
  		path = buildPathname(path, data)
  		if (fireAsync != null) {
  			fireAsync()
  			var state = options ? options.state : null
  			var title = options ? options.title : null
  			if (options && options.replace) $window.history.replaceState(state, title, routePrefix + path)
  			else $window.history.pushState(state, title, routePrefix + path)
  		}
  		else {
  			$window.location.href = routePrefix + path
  		}
  	}
  
  	var currentResolver = sentinel, component, attrs, currentPath, lastUpdate
  	var route = function(root, defaultRoute, routes) {
  		if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined")
  		// 0 = start
  		// 1 = init
  		// 2 = ready
  		var state = 0
  
  		var compiled = Object.keys(routes).map(function(route) {
  			if (route[0] !== "/") throw new SyntaxError("Routes must start with a `/`")
  			if ((/:([^\/\.-]+)(\.{3})?:/).test(route)) {
  				throw new SyntaxError("Route parameter names must be separated with either `/`, `.`, or `-`")
  			}
  			return {
  				route: route,
  				component: routes[route],
  				check: compileTemplate(route),
  			}
  		})
  		var onremove, asyncId
  
  		fireAsync = null
  
  		if (defaultRoute != null) {
  			var defaultData = parsePathname(defaultRoute)
  
  			if (!compiled.some(function (i) { return i.check(defaultData) })) {
  				throw new ReferenceError("Default route doesn't match any known routes")
  			}
  		}
  
  		function resolveRoute() {
  			// Consider the pathname holistically. The prefix might even be invalid,
  			// but that's not our problem.
  			var prefix = $window.location.hash
  			if (routePrefix[0] !== "#") {
  				prefix = $window.location.search + prefix
  				if (routePrefix[0] !== "?") {
  					prefix = $window.location.pathname + prefix
  					if (prefix[0] !== "/") prefix = "/" + prefix
  				}
  			}
  			// This seemingly useless `.concat()` speeds up the tests quite a bit,
  			// since the representation is consistently a relatively poorly
  			// optimized cons string.
  			var path = prefix.concat()
  				.replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent)
  				.slice(routePrefix.length)
  			var data = parsePathname(path)
  
  			assign(data.params, $window.history.state)
  
  			for (var i = 0; i < compiled.length; i++) {
  				if (compiled[i].check(data)) {
  					var payload = compiled[i].component
  					var route = compiled[i].route
  					var update = lastUpdate = function(routeResolver, comp) {
  						if (update !== lastUpdate) return
  						component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div"
  						attrs = data.params, currentPath = path, lastUpdate = null
  						currentResolver = routeResolver.render ? routeResolver : null
  						if (state === 2) mountRedraw.redraw()
  						else {
  							state = 2
  							mountRedraw.redraw.sync()
  						}
  					}
  					if (payload.view || typeof payload === "function") update({}, payload)
  					else {
  						if (payload.onmatch) {
  							Promise.resolve(payload.onmatch(data.params, path, route)).then(function(resolved) {
  								update(payload, resolved)
  							}, function () {
  								if (path === defaultRoute) throw new Error("Could not resolve default route " + defaultRoute)
  								setPath(defaultRoute, null, {replace: true})
  							})
  						}
  						else update(payload, "div")
  					}
  					return
  				}
  			}
  
  			if (path === defaultRoute) throw new Error("Could not resolve default route " + defaultRoute)
  			setPath(defaultRoute, null, {replace: true})
  		}
  
  		if (supportsPushState) {
  			onremove = function() {
  				$window.removeEventListener("popstate", fireAsync, false)
  			}
  			$window.addEventListener("popstate", fireAsync = function() {
  				if (asyncId) return
  				asyncId = callAsync(function() {
  					asyncId = null
  					resolveRoute()
  				})
  			}, false)
  		} else if (routePrefix[0] === "#") {
  			onremove = function() {
  				$window.removeEventListener("hashchange", resolveRoute, false)
  			}
  			$window.addEventListener("hashchange", resolveRoute, false)
  		}
  
  		return mountRedraw.mount(root, {
  			onbeforeupdate: function() {
  				state = state ? 2 : 1
  				return !(!state || sentinel === currentResolver)
  			},
  			oncreate: resolveRoute,
  			onremove: onremove,
  			view: function() {
  				if (!state || sentinel === currentResolver) return
  				// Wrap in a fragment to preserve existing key semantics
  				var vnode = [Vnode(component, attrs.key, attrs)]
  				if (currentResolver) vnode = currentResolver.render(vnode[0])
  				return vnode
  			},
  		})
  	}
  	route.set = function(path, data, options) {
  		if (lastUpdate != null) {
  			options = options || {}
  			options.replace = true
  		}
  		lastUpdate = null
  		setPath(path, data, options)
  	}
  	route.get = function() {return currentPath}
  	route.prefix = function(prefix) {routePrefix = prefix}
  	var link = function(options, vnode) {
  		vnode.dom.setAttribute("href", routePrefix + vnode.attrs.href)
  		vnode.dom.onclick = function(e) {
  			if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return
  			e.preventDefault()
  			e.redraw = false
  			var href = this.getAttribute("href")
  			if (href.indexOf(routePrefix) === 0) href = href.slice(routePrefix.length)
  			route.set(href, undefined, options)
  		}
  	}
  	route.link = function(args) {
  		if (args.tag == null) return link.bind(link, args)
  		return link({}, args)
  	}
  	route.param = function(key) {
  		if(typeof attrs !== "undefined" && typeof key !== "undefined") return attrs[key]
  		return attrs
  	}
  
  	return route
  }
  