/*
---

script: moohistory.js

description: MooHistory is a URL hash wrapper for MooTools which is designed to emulate the onhashchange event for all A grade browsers.

license: MIT-style license.

copyright: Copyright (c) 2009 [Matias Niemela](http://www.yearofmoo.com/).

...
*/
var MooHistory = new Class({

	/*
	-- Options

    - requiresSlash (boolean, defaults to true)
	This will force the URL formats to have a forward slash infront of the hash symbol in the format /page/#/hashexample/.
    
	- interval (integer, defaults to 400)
	The total amount of milliseconds between URL checks. This value does not apply to browsers that support the onhashchange event. Increasing this value will create longer delays between page changes if the back and forward buttons (or if the URL is modified manually) are pressed.
    
	- autoSlash (boolean, defaults to true)
	This will automatically add the forward slash at the beginning and the end of the hash value.
    
	- skipFirstHash (boolean, defaults to false)
	This will skip the hash that exists in the URL as the page loads (even if its empty).

    - allowEmptyHash (boolean, defaults to true)
	This will allow the use of an empty hash (this is useful for a default page).

    - forceEmptyPath (boolean, defalts to false)
	This will force the URL to always be at its root (just domain.com/). Each time a hash value is changed it will always be targeted to the root (i.e. domain.com/#/home/). If a page is loaded with an existing path value (for example. domain.com/home/) then the script will redirect the page to the root of the domain with the path value as the hash value (i.e. domain.com/#/home).DO NOT ENABLE THIS OPTION if you are not 100% sure that you want all your path values to be ingored.

    - ieFramePath (String, defaults to �./ieframe.html�)
	The path of the empty frame that will be used by IE6 and IE7.

    - ignoreClassName (String, defaults to null)
	Links using this className will be ignored and will follow through normally.

    - ignoreBodyClassName (String, defaults to null)
	If the body element contains this class, then all the links will be ignored and the URL hash changing will not be enabled for this page.

    - ignoreIE6 (boolean, defaults to false)
	Whether or not to ignore IE6 completely.

    - restoreOnInvalid (boolean, defaults to false)
	If an invalid hash format is used (i.e. a normal anchor change) then the change will be discarded and the previous hash will be restored.

    - storageKeyName (String, defaults to �storageKeyName�)
	This will be the key that will be used for the Element.storage flag that identifies that the link already has a listener.

    - links (String, defaults to �a�)
	The selector used for fetch all the links on the page and add the URL changing listeners.


	-- Events

	- onHashload
	returns (hash)
	{
		raw:string,
		hash:string,
		preious:string,
		saved:boolean,
		first:boolean,
		empty:boolean
	}
	*/

	Implements:[Events,Options],

	options:{
		requiresSlash:true,
		interval:400,
		autoSlash:true,
		skipFirstHash:false,
		allowEmptyHash:true,
		ieFramePath:'./ieframe.html',
		forceEmptyPath:false,
		ignoreClassName:'',
		ignoreBodyClassName:'',
		ignoreIE6:false,
		restoreOnInvalid:false,
		storageKeyName:'moohistory-link',
		links:'a'
	},

	init:function(options) {

		//set the default stuff
		this.setOptions(options);
		this.clearStack();

		//check the body
		var b = this.options.ignoreBodyClassName;
		if(b && b.length>0 && $(document.body).hasClass(b)) {
			this.init = $empty;
			return;
		}

		//defaults
		this.isIE8 = this.isIE7 = this.isIE6 = false;

		//set the flags
		var t = Browser.Engine.trident;
		if(t) { //ie
			this.isIE8 = typeof XDomainRequest != "undefined";
			this.isIE6 = t.version <= 4;
			this.isIE7 = !this.isIE8 && !this.isIE6;
		}
		this.isIE6or7 = this.isIE6 || this.isIE7;

		//check ie6
		if(this.options.ignoreIE6 && this.isIE6) return;

		//new browsers
		this.hasHashChangeListener = "onhashchange" in window;

		//create the frame
		if(this.hasHashChangeListener) {
			//new browsers don't need the interval loop
			window.addEvent('domready',function() {
				var fn = window.onhashchange || $empty;
				window.onhashchange = function() {
					fn();
					this.change(window.location.hash);
				}.bind(this);
			}.bind(this));
		}
		else {
			if(this.isIE6or7) {
				this.frame = new IFrame({
					src:this.options.ieFramePath,
					width:0,
					height:0,
					onload:function() {
						//get the frame hash
						var url = new String(this.frame.contentWindow.location);
						var uri = new MooHistoryHash(url);
						var path = uri.getSearch();
						this.goto(path);
					}.bind(this)
				}).injectInside(document.body);
			}

			//setup the opera history manager
			if(window.opera) {
				history.navigationMode='compatible';
			}

			//setup the listener
			this.loop();
		}

		//check links
		if(this.options.links) {
			this.setLinks(this.options.links);	
		}

		//this isn't needed anymore
		this.init = $empty;
	},

	setLinks:function(links) {
		this.options.links = links;
		this.updateLinks();
	},

	updateLinks:function() {

		//setup the ignore class
		var selector = this.options.links;
		if(!selector || selector.length==0)
		selector = document.links;
		var links = $$(selector);
		var that = this;
		var storageName = this.options.storageKeyName;
		var ignoreClass = this.options.ignoreClassName;
		if(ignoreClass.length==0) ignoreClass = null;

		//apply the listeners
		links.each(function(elm) {
			if(elm.retrieve(storageName) || (ignoreClass && elm.hasClass(ignoreClass))) return;

			//setup the event
			elm.store(storageName,true);
			elm.addEvent('click',function(event) {
				//check ignore
				event.preventDefault();
				var target = $(event.target);
				var link = target.getProperty('href');
				this.goto(link);
			}.bind(that));
		});
	},

	pause:function() {
		if(this.loopInstance) {
			$clear(this.loopInstance);
			this.loopInstance = null;
		}
	},

	loop:function() {
		this.pause();
		this.loopInstance = this.listen.delay(this.options.interval,this);
	},

	listen:function() {
		var hash = (hash || this.getHash() || '').trim();
		if(!this.compare(hash)) {
			this.goto(hash);
		}
		this.loop();
	},

	compare:function(hash) {
		return hash == this.current;
	},

	change:function(hash) {
		//pause for now
		hash = hash.trim();
		this.pause();

		//check the frame
		if(this.isIE6or7) {
			var frame = this.getFrame().contentWindow;
			var url = new String(frame.location);
			var uri = new MooHistoryHash(url);
			var search = uri.getSearch();
			if(search!=hash) {
				uri.setSearch(hash);
				url = uri.getURL();
				frame.location = url;
			}
		}

		//setup the output
		var decoded = this.decodeHash(hash);
		var current = this.current || null;
		var isFirst = !(this.stack&&this.stack.length>0);
		var isSaved = !isFirst && this.stack.indexOf(hash);
		this.current = hash;
		this.stack.push(hash);

		//check the first hash
		if(!(isFirst && this.options.skipFirstHash)) {

			//setup the output
			var isEmpty = decoded.length==0;
			if(current) current = this.decodeHash(current);
			var output = {
				'raw':hash,
				'hash':decoded,
				'previous':current,
				'saved':isSaved,
				'first':isFirst,
				'empty':isEmpty
			};

			//fire the events
			window.fireEvent('hashunload',[current,decoded]);
			window.fireEvent('hashload',[output]);
		}

		//resume
		this.loop();
	},

	getFrame:function() {
		return this.frame || null;
	},

	_onInvalidHash:function(hash) {
		window.fireEvent('invalidHash',[hash]);
		if(this.options.restoreOnInvalid) {
			if(this.current && this.current != hash)
			this.goto(this.current);
		}
		else
		this.current = hash;
	},

	_onEmptyHash:function() {
		if(this.options.allowEmptyHash) {
			this.change('');	
		}
	},

	goto:function(page) {
		if(this.isIE6or7) {
			this.goto = function(page) {
				page = page.charAt(0) != '#' ? '#'+page : page;
				if(page.length<=1) {
					this._onEmptyHash();
					return;
				}
				page = this.encodeHash(page);
				if(this.compare(page)) {
					return;
				}
				if(!this.isProperHash(page)) {
					this._onInvalidHash(page);
					return;
				}
				var url = this.getURL();
				var uri = new MooHistoryHash(url);
				uri.setHash(page);
				uri.go();
				this.change(page);
			}.bind(this);
		}
		else {
			this.goto = function(page) {
				page = page.charAt(0) != '#' ? '#'+page : page;
				if(page.length<=1) {
					this._onEmptyHash();
					return;
				}
				page = this.encodeHash(page);
				if(!this.isProperHash(page)) {
					this._onInvalidHash(page);
					return;
				}
				window.location.hash = page;
				if(!this.hasHashChangeListener)
				this.change(page);
			}.bind(this);
		}
		this.goto(page);
	},

	restore:function() {
		if(this.current) {
			this.goto(this.current);
		}
	},

	isProperHash:function(hash) {
		return !this.options.autoSlash || hash.charAt(0) == '#' && hash.charAt(1)=='/';
	},

	encodeHash:function(hash) {
		if(hash.charAt(0)=='#') {
			hash = hash.substr(1);	
		}
		if(this.options.autoSlash && hash.charAt(0)!='/') {
			hash = '/'+hash;
		}
		hash = '#'+hash;
		return hash;
	},

	decodeHash:function(hash) {
		if(hash.charAt(0)=='#') {
			hash = hash.substr(1);	
		}
		if(this.options.autoSlash) {
			var l = hash.length-1;
			if(hash.charAt(l)=='/')
			hash = hash.substr(0,l);
			if(hash.charAt(0)=='/')
			hash = hash.substr(1);
		}
		return hash;
	},

	getStack:function() {
		return this.stack;
	},

	clearStack:function() {
		this.stack = [];
	},

	getURL:function() {
		return new String(window.location);
	},

	getHash:function() {
		if(this.isIE6or7) {
			this.getHash = function() {
				var url = this.getURL();
				return MooHistoryHash.getHash(url);
			}.bind(this);
		}
		else {
			this.getHash = function() {
				return new String(window.location.hash);
			};
		}
		return this.getHash();
	},

	first:function() {
		var pos = 1;
		var stack = this.getStack();
		if(pos>0 && stack.length>0) {
			var page = stack[pos-1];
			if(page) {
				this.goto(page);	
			}
		}
	},

	back:function() {
		var pos = this.position || 0;
		var stack = this.getStack();
		if(pos>0 && stack.length>0) {
			var page = stack[pos-1];
			if(page) {
				this.goto(page);	
			}
		}
	},

	forward:function() {
		var pos = this.position || 0;
		var stack = this.getStack();
		if(pos>0 && stack.length>0) {
			var page = stack[pos+1];
			if(page) {
				this.goto(page);	
			}
		}
	},

	destroy:function() {
		//clear the interval
		this.pause();

		//remove the methods
		this.loop = this.pause = this.goto = this.change = this.listen = $empty;

		//remove the events
		window.removeEvent('hashload');
		window.removeEvent('hashunload');
		window.removeEvent('invalidHash');

		//remove the frame
		var frame = this.getFrame();
		if(frame) {
			$(frame).destroy();
		}

		//remove the instance
		(function() {
			delete MooHistory;
			delete MooHistoryHash;
		}).delay(100);
	}
});

var MooHistoryHash = new Class({

	initialize:function(url) {
		this.url = unescape(url || new String(window.location));
	},

	getPath:function() {
		var url = this.getURL();
		var pos = url.indexOf('?');
		var path = url;
		if(pos>0)
		path = url.substr(0,pos);
		return path;
	},

	setHash:function(hash) {
		var current = this.getHash();
		var url = this.getURL();
		if(current)
		url = url.replace(current,hash);
		else
		url += hash;
		this.url = url;
		this.hash = hash;
	},

	getHash:function() {
		if(!this.hash) {
			this.hash = MooHistoryHash.getHash(this.getURL());
		}
		return this.hash;
	},

	go:function() {
		var url = this.getURL();
		window.location = url;
	},

	getSearch:function() {
		if(!this.search) {
			var url = this.getURL();
			var from = url.indexOf('?');
			var to = url.indexOf('#');
			if(from>=0) {
				from++;
				if(to>=0)
				this.search = url.substring(from,to);
				else
				this.search = url.substr(from);
			}
			else
			this.search = '';
		}
		return this.search;
	},

	setSearch:function(search) {
		var path = this.getPath();
		if(search.charAt(0)=='#')
		search = search.substr(1);
		path += '?'+search;
		this.url = path;
	},

	getURL:function() {
		return this.url;	
	},

	toString:function() {
		return this.getURL();	
	}
});

MooHistoryHash.getHash = function(uri) {
	var pos = uri.indexOf('#');
	if(pos>=0)
	return uri.substr(pos);
	return null;
};

//lets make it a struct
MooHistory = new MooHistory;

Element.Events.hashload = {
	onAdd:function() {
		MooHistory.init();
	}
}

Element.Events.hashunload = {
	onAdd:function() {
		MooHistory.init();
	}
}