dojo.provide("dojox.html.hash");

	// summary: 
	//		Methods for monitoring and updating the hash in the browser URL.
	//
	// example:
	//		dojox.html.addOnHashChange(callback);
	//
	//		function callback (hashObj) {
	//			//parse the hashObj (key/value pairs) and update UI.
	// 		}
	//
	//		function sortByTitle () {  //example of non-destructive hash update
	//			var hashObj = dojox.html.hash();
	//			hashObj.sortTitle = 1;
	//			dojox.html.hash(hashObj);
	//		}
	//

(function() {
	dojox.html.addOnHashChange = function(callback) {
		// summary:
		//		Registers a callback to be notified when the hash changes. Passes hashObj as first parameter.
		if (dojo.isFunction(callback)) {
			_onchangeArr.push(callback);
		} else {
			console.error("dojox.html.addOnHashChange: Callback is not a function");
		}
	};
	
	dojox.html.hash = function(/* String | Object | Array */ hash) {
		//	summary:
		//		Gets or sets the hash string.
		//	description:
		//		Handles normalized getting and setting of location.hash.
		//	
		//		If no arguments are passed, acts as a getter.
		//
		//		If a string or an object is passed, acts as a setter.
		//	hash: 
		//		String: the hash is set - #string.
		//		Object: the hash is set using query notation - #param1=val&param2=val
		//		Array:  the hash is set using slash notation - #/param1/param2/param3/
		//		
		//	returns:
		//		when used as a getter, returns the current hash string.
		//		when used as a setter, undefined.
		
		//getter
		if (arguments.length == 0) return _getHash();
		
		//setter
		var href = "#";
		if (dojo.isString(hash)) href += encodeURIComponent(hash);
		else if (dojo.isArray(hash)) href += "/" + hash.join("/");
		else if (dojo.isObject(hash)) href += dojo.objectToQuery(hash);
		location.href = href;
	};
	

	// Global vars
	
	var _onchangeArr = [];
	var _recentHash = null;		//only needed for non-IE browsers	

	//Internal functions

	function _getHash() {
		var hashString = window.location.hash;
		if (hashString.substring(0,1) == "#")
			return decodeURIComponent(hashString.substring(1));
		return decodeURIComponent(hashString);
	};

	function _onHashChange(event){
		// When the hash changes, call the registered callbacks.
		for (var i = 0; i < _onchangeArr.length; i++) {
			if (dojo.isFunction(_onchangeArr[i])) _onchangeArr[i](event);
		}
	};
	
	function _pollLocation(){
		if (_getHash() === _recentHash) {	
			return;
		}
		_recentHash = _getHash();
		_onHashChange();
	};

	function _replaceAfterToken(string, token, replacement) {
		if (!string || !token || replacement == null) {
			throw new Error("dojox.html.hash internal error: params cannot be null in _replaceAfterToken");
		}
		if(string.indexOf(token) == -1) {
			return string + token + replacement;
		}
		return string.slice(0, string.indexOf(token) + 1) + replacement;
	};

	function _setUriFragment(uri, fragmentString) {
		return _replaceAfterToken(uri, "#", fragmentString);
	};

	function _setUriQuery(uri, queryString) {
		return _replaceAfterToken(uri, "?", queryString);
	};

	function IEUriMonitor () {
		// summary:
		//		Determine if the browser's URI has changed or if the user has pressed the 
		//		back or forward button. If so, call _onHashChange.
		//	description:
		//		IE doesn't add changes to the URI's hash into the history unless the hash
		//		value corresponds to an actual named anchor in the document. To get around
		//      this IE difference, we use a background IFrame to maintain a back-forward
		//		history, by updating the IFrame's query string to correspond to the
		//		value of the main browser location's hash value.
		//
		//		E.g. if the value of the browser window's location changes to
		//
		//		#action=someAction
		//
		//		... then we'd update the IFrame's source to:
		//
		//		?action=someAction
		//
		//		This design leads to a somewhat complex state machine, which is
		//		described below:
		//
		//		s1: Stable state - neither the window's location has changed nor
		//			has the IFrame's location. Note that this is the 99.9% case, so
		//			we optimize for it.
		//			Transitions: s1, s2, s3
		//		s2: Window's location changed - when a user clicks a hyperlink or
		//			code programmatically changes the window's URI.
		//			Transitions: s4
		//		s3: IFrame's location changed as a result of user pressing back or
		//			forward - when the user presses back or forward, the location of
		//			the background's IFrame changes to the previous or next value in
		//			its history.
		//			Transitions: s1
		//		s4: IEUriMonitor has programmatically changed the location of the
		//			background IFrame, but it's location hasn't yet changed. In this
		//			case we do nothing because we need to wait for the IFrame's
		//			location to reflect its actual state.
		//			Transitions: s4, s5
		//		s5:	IEUriMonitor has programmatically changed the location of the
		//			background IFrame, and the IFrame's location has caught up with
		//			reality. In this case we need to transition to s1.
		//			Transitions: s1
		//

		//create and append iframe
		var IFRAME_ID = "dojo-hash-iframe";
		var iframeNode = document.createElement("iframe");
		var uri = new dojo._Url(location.href);
		iframeNode.id = IFRAME_ID;
		iframeNode.src = dojo.moduleUrl("dojox.html", "hashiframe.html" + (uri.fragment ? "?" + uri.fragment : ""));
		iframeNode.style.display = "none";
		document.body.appendChild(iframeNode);

		// shortcuts to the objects we'll be using a lot
		var iframeWindow = window[IFRAME_ID];
		var iframeLoc = iframeWindow.location;
		var winLoc = window.location;

		// declare vars in outer function so that they're shared by inner functions
		var _recentHash, recentIframeQuery, isIframeTransitioning, expectedIFrameQuery;

		function resetState() {
			_recentHash = winLoc.hash;
			recentIframeQuery = iframeLoc.search;
			isIframeTransitioning = false;
			expectedIFrameQuery = null;
		}
		// initialize state (transition to s1)
		resetState();
		this.pollLocation = function() {

			// check to see if we're in an IFrame location transition (s4 or s5)
			if(isIframeTransitioning && _recentHash === winLoc.hash) {
				// if the IFrame's location has caught up (s5), transition back to
				// the stable state (s1)
				if (iframeLoc.search === expectedIFrameQuery) {
					resetState();
				}
				// whether we were in s4 or s5, return
				return;
			}
			// if we're still in a stable state (s1), do nothing
			if(_recentHash === winLoc.hash && recentIframeQuery === iframeLoc.search) {
				return;
			}

			// if we're still going, either the window's location has changed (s2)
			// or the IFrame's location has changed as a result of the user clicking
			// the back or forward button (s3)
			if(_recentHash !== winLoc.hash) {
				_recentHash = winLoc.hash;
				// s2 (window location changed)
				var locationUri = new dojo._Url(winLoc.href);
				// transition to s4
				isIframeTransitioning = true;
				expectedIFrameQuery = _setUriQuery(iframeLoc.search || "?", locationUri.fragment || "");
				iframeNode.src = _setUriQuery(iframeNode.src, locationUri.fragment || "");
			} else {
				// s3 (iframe's location changed via user action)
				var iframeUri = new dojo._Url(iframeLoc.href);
				// update the browser's URI
				winLoc.href = _setUriFragment(winLoc.href, iframeUri.query || "");
				// transition to s1
				resetState();
			}
			_onHashChange();
			return;
		}
		setInterval(this.pollLocation, 200);
	};
	
	dojo.addOnLoad(function() {
		if ("onhashchange" in dojo.body() && (!dojo.isIE || dojo.isIE >= 8)) {	//need this IE browser test because "onhashchange" exists in IE8 in IE7 mode
			dojo.connect(document.body,"onhashchange",null,_onHashChange);
		} else {
			if (dojo.isIE) {
				new IEUriMonitor(); //Use hidden iframe in versions of IE that don't have onhashchange event
			} else {
				_recentHash = _getHash();
				setInterval(_pollLocation, 200); //Poll the window location every 200 ms in other browsers.
			}
		}
	});
})();