/**
 * @fileOverview
 *
 * @description Harald Kirschner's <a target="_BLANK" href="http://digitarald.de/project/history-manager/">HistoryManager</a>
 * 
 * @version		1.0rc2
 * 
 * @see			Events, Options
 * 
 * @license		MIT License
 * @author		Harald Kirschner &lt;mail [at] digitarald.de&gt;
 * @author      peterpunk (update to MooTools 1.2.* - <a target="_BLANK" href="http://gist.github.com/129829">http://gist.github.com/129829</a>)
 * @author      Ludvig Svenonius (doc comment adaptations for JsDoc Toolkit)
 * @copyright	2007 Harald Kirschner
 */

var HistoryManagerX = new Class (
   /** @lends HistoryManagerX */
  {

  Implements: [Options, Events],

	/**
	 * <p>Default options - Can be overridden with setOptions</p>
	 *
	 * <ul>
	 *  <li>observeDelay: Duration for checking the state, default 100ms</li>
	 *  <li>stateSeparator: Seperator for module-state join, default ';'</li>
	 *  <li>iframeSrc: Scr for IE6/7 iframe, must exist on server!</li>
	 *  <li>onStart: Fires on start</li>
	 *  <li>onRegister: Fires on register</li>
	 *  <li>onUnregister: Fires on unregister</li>
	 *  <li>onUpdate: Fires when state changes from ...
	 *  <li>onStateChange: ... module changes</li>
	 *  <li>onObserverChange: ... history change</li>
	 * </ul>
	 */
	options: {
		observeDelay: 100,
		stateSeparator: ';',
		iframeSrc: 'blank.html',
		onStart: Class.empty,
		onRegister: Class.empty,
		onUnregister: Class.empty,
		onStart: Class.empty,
		onUpdate: Class.empty,
		onStateChange: Class.empty,
		onObserverChange: Class.empty
	},

	/**
	 * <p>Default options for register</p>
	 *
	 * <ul>
	 *  <li>defaults: Default values array, initially empty.</li>
	 *  <li>regexpParams: When regexp is a String, this is the second argument for new RegExp.</li>
	 *  <li>skipDefaultMatch: default true; When true onGenerate is not called when current values are similar to the default values.</li>
	 * </ul>
	 */
	dataOptions: {
		skipDefaultMatch: true,
		defaults: [],
		regexpParams: ''
	},

	/**
	 * @return	this
	 * 
	 * @param	{Object} options Options for this HistoryManager instance. Supported options:
	 * <ul>
	 *  <li>observeDelay: duration in ms, default 100 - BackBuddy observe the hash for changes periodical</li>
	 *  <li>stateSeparator: char, default ';' - Separator for multiple module-states in the hash</li>
	 *  <li>iframeSrc: string, default 'blank.html' - File for the iframe (IE6/7), must exist on the server!</li>
	 *  <li>Events: onStart, onRegister, onStart, onUpdate, onStateChange, onObserverChange</li>
	 * </ul>
	 * @class Observes back/forward button usage and saves states
	 * for registered modules into the hash. This allows to
	 * bookmark specific states for an application.
	 * @constructs
	 */
	initialize: function(options) {
		if (this.modules) return this;
		this.setOptions(options);
		this.modules = $H({});
		this.count = history.length;
		this.states = [];
		this.states[this.count] = this.getHash();
		this.state = null;
		return this;
	},

	/**
	 * <p>Check hash and start observer.</p>
	 * 
	 * <p>Call start after registering ALL modules. This start the observer,
	 * reads the state from the hash and calls onMatch for effected modules.</p>
	 * 
	 * @return	this
	 * 
	 */
	start: function() {
		this.observe.periodical(this.options.observeDelay, this);
		this.started = true;
		this.observe();
		this.update();
		this.fireEvent('onStart', [this.state]);
		return this;
	},

	/**
	 * <p>Registers a module</p>
	 * 
	 * @return	{Object} Object with shortcuts for setValues, setValue, generate and unregister.
	 * 
	 * @param	{String} 		key 		Module name/key
	 * @param	{Array} 		defaults 	Default values, the input values given to onMatch and onGenerate will be complemented with these.
	 * @param	{Function} 		onMatch 	Will be called when the regexp matches, with the new values as argument.
	 * @param	{Function}      onGenerate 	Should return the string for the state string, values are first argument.
	 * @param	{RegExp/String} regexp 		Regular expression that matches the string updated from onGenerate
	 * @param	{Object} 		options 	Options for this module. (optional)
	 */
	register: function(key, defaults, onMatch, onGenerate, regexp, options) {
		if (!this.modules) this.initialize();
		var data = $merge(this.dataOptions, options || {}, {
			defaults: defaults,
			onMatch: onMatch,
			onGenerate: onGenerate,
			regexp: regexp
		});
		data.regexp = data.regexp || key + '-([\\w_-]*)';
		if (typeof data.regexp == 'string') data.regexp = new RegExp(data.regexp, data.regexpParams);
		data.onGenerate = data.onGenerate || function(values) { return key + '-' + values[0]; };

		data.values = data.defaults.copy();
		this.modules.set(key, data);
		this.fireEvent('onUnregister', [key, data]);
		return {
			setValues: function(values) {
				return this.setValues(key, values);
			}.bind(this),
			setValue: function(index, value) {
				return this.setValue(key, index, value);
			}.bind(this),
			generate: function(values) {
				return this.generate(key, values);
			}.bind(this),
			unregister: function() {
				return this.unregister(key);
			}.bind(this)
		};
	},

	/**
	 * <p>Removes an module from the HistoryManager.</p>
	 * 
	 * @param	{String} key Module name/key
	 */
	unregister: function(key) {
		this.fireEvent('onRegister', [key]);
		this.modules.remove(key);
	},

	/**
	 * <p>Set all values new, updates new state.</p>
	 * 
	 * @param	{String} key    Module name/key
	 * @param	{Object} values Complete values
	 */
	setValues: function(key, values) {
		var data = this.modules.get(key);
		if (!data || data.values.isSimilar(values)) return this;
		data.values = values;
		this.update();
		return this;
	},

	/**
	 * <p>Set one value, updates new state.</p>
	 * 
	 * @param	{String} key   Module key
	 * @param	{Number} index Value index
	 * @param	{Object} value Value
	 */
	setValue: function(key, index, value) {
		var data = this.modules.get(key);
		if (!data || data.values[index] == value) return this;
		data.values[index] = value;
		this.update();
		return this;
	},

	/**
	 * <p>Generates a hash from the given values.</p>
	 * 
	 * @param	{String} key    Module name/key
	 * @param	{Array}  values Values
	 */
	generate: function(key, values) {
		var data = this.modules.get(key);
		var current = data.values.copy();
		data.values = values;
		var state = this.generateState();
		data.values = current;
		return '#' + state;
	},

	observe: function() {
		if (this.timeout) return;
		var state = this.getState();
		if (this.state == state) return;
                if (state[0]=='!'){
                    return;
                }
		if (((Browser.Engine.trident &&(!document.querySelectorAll))) && (this.state !== null)) this.setState(state, true);
		else this.state = state;
		this.modules.each(function(data, key) {
			var bits = state.match(data.regexp);
			if (bits) {
				bits.splice(0, 1);
				bits.complement(data.defaults);
				if (!bits.isSimilar(data.defaults)) data.values = bits;
			} else data.values = data.defaults.copy();
			data.onMatch(data.values, data.defaults);
		});
		this.fireEvent('onStateChange', [state]).fireEvent('onObserverChange', [state]);
	},

	generateState: function() {
		var state = [];
		this.modules.each(function(data, key) {
			if (data.skipDefaultMatch && data.values.isSimilar(data.defaults)) return;
			state.push(data.onGenerate(data.values));
		});
		return state.join(this.options.stateSeparator);
	},

	update: function() {
		if (!this.started) return this;
		var state = this.generateState();
		if ((!this.state && !state) || (this.state == state)) return this;
		this.setState(state);
		this.fireEvent('onStateChange', [state]).fireEvent('onUpdate', [state]);
		return this;
	},

	observeTimeout: function() {
		if (this.timeout) this.timeout = $clear(this.timeout);
		else this.timeout = this.observeTimeout.delay(200, this);
	},

	getHash: function() {
		var href = top.location.href;
		var pos = href.indexOf('#') + 1;
		return (pos) ? href.substr(pos) : '';
	},

	getState: function() {
		var state = this.getHash();
		if (this.iframe) {
			var doc = this.iframe.contentWindow.document;
			if (doc && doc.body.id == 'state') {
				var istate = doc.body.innerText;
				if (this.state == state) return istate;
				this.istateOld = true;
			} else return this.istate;
		}
		/*
		if (Browser.Engine.webkit && history.length != this.count) {
			this.count = history.length;
			return $pick(this.states[this.count - 1], state);
		}
		*/
		return state;
	},

	setState: function(state, fix) {
                tmp = this.getState();
                if (tmp[0]=='!') return;
		state = $pick(state, '');
		/* removed support for Safari 2 temporaly
		if (Browser.Engine.webkit) {
			if (!this.form) this.form = new Element('form', {method: 'get'}).injectInside(document.body);
			this.count = history.length;
			this.states[this.count] = state;
			this.observeTimeout();
			this.form.setProperty('action', '#' + state).submit();
			
		} else top.location.hash = state || '#';
		*/
		top.location.hash = state || '#';
		if (Browser.Engine.trident && (!document.querySelectorAll) && (!fix || this.istateOld)) {
			if (!this.iframe) {
				this.iframe = new Element('iframe', {
					src: this.options.iframeSrc,
					style: 'visibility: hidden;height:1px;'
				}).injectInside(document.body);
				this.istate = this.state;
			}
			try {
				var doc = this.iframe.contentWindow.document;
				doc.open();
				doc.write('<html><body id="state">' + state + '</body></html>');
				doc.close();
				this.istateOld = false;
			} catch(e) {};
		}
		this.state = state;
	},

	extend: $extend
});

/**
 * Extends Array with 2 helpers: isSimilar(array) and complement(array)
 * 
 */
Array.implement(
	/** @lends Array */
	{

	/**
	 * <p>Returns true for type-insensitively equal arrays.</p>
	 * 
	 * @example
	 * [1].isSimilar(['1']) == true
	 * [1, 2].isSimilar([1, false]) == false
	 *  
	 * @return	{Boolean}
	 * @param	{Object} array The {@link Array} to compare <i>this</i> with.
	 */
	isSimilar: function(array) {
		return (this.toString() == array.toString());
	},

	/**
	 * <p>Fills up empty array values from another {@link Array}, length is the same</p>
	 * 
	 * @example
	 * [1, null].complement([3, 4]) == [1, 4]
	 * [undefined, '1'].complement([2, 3, 4]) == [2, '1']
     *
	 * @return	{Array} this
	 * @param	{Object} array The {@link Array} to complement <i>this</i> with.
	 */
	complement: function(array) {
		for (var i = 0, j = this.length; i < j; i++) this[i] = $pick(this[i], array[i] || null);
		return this;
	}, 
	
	/**
	 * <p>Returns a copy of the array</p>
	 * 
	 * @example
	 * var letters = ["a","b","c"];
	 * var copy = letters.copy();    // ["a","b","c"] (new instance)
     *
	 * @return	{Array} A copy of the array, starting from <i>start</i>, and with length <i>length</i>.
	 * @param	{integer} start  The index where to start the copy, default is 0. If negative, it is taken as the offset from the end of the array. (optional)
	 * @param   {integer} length The number of elements to copy. By default, copies all elements from start to the end of the array. (optional)
	 */
  copy: function(start, length){
    start = start || 0;
    if (start < 0) start = this.length + start;
    length = length || (this.length - start);
    var newArray = [];
    for (var i = 0; i < length; i++) newArray[i] = this[start++];
    return newArray;
  }
});

var HistoryManager;
//window.addEvent('domready', function(){

HistoryManager = new HistoryManagerX();
//});

