/*
---
description: A Class that provides a cross-browser history-management functionaility, using the browser hash to store the application's state

license: MIT-style

authors:
- Arieh Glazer

requires:
- HistoryManager/0.4.1: HashListner

provides: [HistoryManager]

...
*/

/*
 * modified by Robert.Schulze@dotless.de :
 *  - added a `has` method to test if a key exists
 *  - implemented a different "hash" (url-fragment) format to use a query-string-like syntax.
 *    Syntax is compatible to the String.toQueryParams() / Object.toQueryString() of Prototype 1.6. (In fact, it's a reimplementation of the relevant parts)
 */

/**
 * class HistoryManager < HashListener
 *
 * This class is meant to function as a domain observer. It uses the browser-hash (via HashListener) to monitor and save the domain-state (enabeling bookmarks and back/farward button functionality). 
 * The class acts as a paramater stack, to monitor and modify the state of an aplication. Whenever a paramater withing the stack changes state, the class notifies it's followers of the change.
**/

/**
 * new HistoryManager(options)
 *     - options (`Object`): _(Optional)_ options as a `key:value` pair
 * 
 * fires key-added, key-changed, key-removed
 * 
 * The class is event-based, but doesn't have prefixed events. The events change according to the key being observed. It is done thus (where 'key' is the name of the key)
 * - 'key-added' : will be fired when an unset key is given a value. will pass the new value as paramater
 * - 'key-changed' : will be fired when a key's value has been changed. will pass the new value as paramater
 * - 'key-removed' : will be fired when a key has been removed from the state. will pas the key's last value as paramater
 *
 * ##### Options:
 * - blank_page (`string`: defaults to `blank.html`): an alternative path to the blank.html file. 
 * - start (`bool`: defaulst to `false`): whether to start the listener on construction
 * 
 * ##### Example:
 *     var HM = new HistoryManager(options);
 * 
**/

/** 
 * HistoryManager#set(key, value)
 *     - key (`String`): A key identifier
 *     - value (`mixed`) whatever value you want to set to that key (must be QueryString encodable)
 * 
 * Used to set a state for the domain. Will set a key's value in the state.
 * 
 * ##### Example:
 * 
 *     HM.set('my-key', "some string");
 * 
**/

/** 
 * HistoryManager#remove(key)
 *     - key (`String`) A key identifier
 * 
 * Used to remove a state from the domain. Will remove `key` and it's value from the state.
 * 
 * ##### Example:
 * 
 *     HM.remove('my-key');
**/

/** 
 * HistoryManager#has(key)
 *     - key (`String`) A key identifier
 * 
 * Used to test if ther is any value from the domain, usefull, when to allow `undefined`
 * 
 * ##### Example:
 * 
 *     HM.has('my-key');
**/

/** 
 * HistoryManager#start()
 * 
 * Will start the listener. This will enable the various events attached to the class.
 * 
 * ##### Example:
 * 
 *     HM.start();
**/

/**
 * HistoryManager#stop()
 * 
 * Will stop the event listener. When down, no events will be fired
 * 
 * ##### Example:
 * 
 *     HM.stop();
**/

/**
 * HistoryManaget#addEvent(eventName, handler)
 *     - eventName (`String`): The name of the event to listen. This is the name of the key plus one of ['-added'|'-changed'|'-removed']
 *     - handler (`function`): The callback function
 * 
 * ##### Example
 *     var HM = new HistoryManager();
 *     
 *     HM.addEvent('MyVar-added'  , function(myvar){console.log('added '+myvar)});
 *     HM.addEvent('MyVar-changed', function(myvar){console.log('changed '+myvar)});
 *     HM.addEvent('MyVar-removed', function(myvar){console.log('removed '+myvar)});
 * 
 *     // it is strongly recomended to set the events before starting the object to get "onstart-changes"
 *     HM.start();
 *     
 *     HM.set('MyVar',1); //will log 'added 1'
 *     HM.set('MyVar',2); //will log 'changed 2'
 *     HM.set('MyVar',3); //will log 'changed 3'
 *     HM.remove('MyVar');//will log 'removed 3'
 *     
 *     // pressing back button will log the following:
 *     // 1. added 3
 *     // 2. changed 2
 *     // 3. changed 1
 *     // 4. removed 1
**/
var HistoryManager = new Class({
	Extends : HashListener,
	state : new Hash({}),
	fromHash : false,
	fromHandle :false,
	initialize : function(options){
		this.parent(options);
		this.addEvent('hash-changed',this.updateState.bind(this));
	},
	updateState : function (hash){
		var self = this;
		hash = new Hash(this.decode(decodeURIComponent(hash)));
		
		this.state.each(function(value,key){
			var nvalue;

			if (hash.has(key)){
				nvalue = hash.get(key);
				self.state.set(key,nvalue);
				self.fireEvent(key+'-changed',nvalue);
			}else{
				nvalue = self.state.get(key);
				self.fireEvent(key+'-removed',nvalue);
				self.state.erase(key);
			}
			
			hash.erase(key);
		});
		
		hash.each(function(value,key){
			self.state.set(key,value);
			self.fireEvent(key+'-added',value);
		});
	},
	set : function(key,value){
		var newState = new Hash(this.state);
		
		newState.set(key, value);

		this.updateHash(this.encode(newState));
		
		return this;
	},
	remove : function(key){
		var newState = new Hash(this.state);
		
		newState.erase(key);
		
		this.updateHash(this.encode(newState));
		
		return this;
	},
	
	has: function (key) {
		return (new Hash(this.state)).has(key);
	},
	
	encondePair: function (key, value) {

		if ("undefined" === typeof value) {
			return key;
		}
		return key + "=" + encodeURIComponent( (null === value ? '' : String(value)) );
	},
	
	encode: function (hsh) {
		
		var encodedPairs = [];
		
		$each(hsh, function (value, key) {
			if ('array' === $type(value)) {
				encodedPairs.extend(value.map(function (val, i) {
					return this.encondePair(key, val);
				}, this));
			} else {
				encodedPairs.push(this.encondePair(key, value));
			}
		}, this);
		
		return encodedPairs.join('&');
	},
	
	decode: function (str, separator) {
		
		var parsed = {};
		var match = str.trim().match(/([^?#]*)(#.*)?$/);
		
	    if (!match) {
			return parsed;
		}
	    
		$each(match[1].split(separator || '&'), function (pair, index) {
			if ((pair = pair.split('='))[0]) {
				var key = decodeURIComponent(pair.shift());
				var value = pair.length > 1 ? pair.join('=') : pair[0];
				
				if (value != undefined) {
					value = decodeURIComponent(value);
				} 
				
				if (key in parsed) {
					if ('array' !== $type(parsed[key])) {
						parsed[key] = [parsed[key]];
					}
					parsed[key].push(value);
				} else {
					parsed[key] = value;
				}
			}
		}, this);
		
		return parsed;
	}
});

