/*
---

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({



	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();
	}
}
