/**
 * HashStorage v1.0.0
 * https://github.com/Wolfy87/HashStorage
 * 
 * Oliver Caldwell (http://oli.me.uk)
 * Creative Commons Attribution 3.0 Unported License (http://creativecommons.org/licenses/by/3.0/)
 */

;(function(exports) {
	/*jshint smarttabs:true*/
	/*global define,module*/
	'use strict';
	
	/**
	 * Adds an event listener with the correct function
	 * 
	 * @param {String} e The event name
	 * @param {Function} listener The listener function
	 */
	function addListener(e, listener) {
		if(exports.addEventListener) {
			exports.addEventListener(e, listener);
		}
		else {
			exports.attachEvent('on' + e, listener);
		}
	}
	
	/**
	 * Removes an event listener with the correct function
	 * 
	 * @param {String} e The event name
	 * @param {Function} listener The listener function
	 */
	function removeListener(e, listener) {
		if(exports.removeEventListener) {
			exports.removeEventListener(e, listener);
		}
		else {
			exports.detachEvent('on' + e, listener);
		}
	}
	
	/**
	 * Watches the URLs hash for changes and merges those changes with an existing object
	 * Allowing storage of complex data in a sharable URL
	 * The data can be accessed via the data property, i.e. `hashStorageInstance.data.foo`
	 */
	function HashStorage() {
		// Setup the events that wait for the hash to change
		this.addEvents();
		
		// Parse the hash for the first time
		this.parseHash();
	}
	
	/**
	 * Attaches the events to the window object that fire when the hash changes
	 * Can be removed with the removeEvents method
	 */
	HashStorage.prototype.addEvents = function() {
		// Initialise variables
		var self = this;
		
		// Set up the listener for the hash change event
		// By storing it the listener can be removed later
		// A wrapper function has to be used to correct the scope of the `this` object
		self.hashChangeListener = function() {
			self.parseHash();
		};
		
		// Scan the hash for JSON commands when it changes
		addListener('hashchange', self.hashChangeListener);
	};
	
	/**
	 * Removes the events set by the addEvents method
	 */
	HashStorage.prototype.removeEvents = function() {
		// Remove the listener if present
		if(this.hashChangeListener) {
			removeListener('hashchange', this.hashChangeListener);
		}
	};
	
	/**
	 * Returns the result of decoding the URLs hash as JSON
	 * If the JSON is invalid or the hash is nothing like JSON then it will return false
	 * 
	 * @return {Object|Boolean} Either the decoded hash object or false if no object could be decoded
	 */
	HashStorage.prototype.getHash = function() {
		// Initialise variables
		var hash = null;
		
		// Try to decode the hash
		try {
			hash = JSON.parse(exports.location.hash.slice(1));
		}
		catch(e) {
			hash = false;
		}
		
		// Return the result
		return hash;
	};
	
	/**
	 * Checks if an object is empty
	 * 
	 * @param {Object} obj The object to check
	 * @return {Boolean} True if the object is empty, false if not
	 */
	HashStorage.prototype.isEmpty = function(obj) {
		// Initialise variables
		var key = null;
		
		// Loop over the object
		// Return false if there is anything in it
		for(key in obj) {
			if(obj.hasOwnProperty(key)) {
				return false;
			}
		}
		
		// Return true if it is empty
		return true;
	};
	
	/**
	 * Encodes the passed object into JSON and sets the URLs hash to it
	 * 
	 * @param {Object} hash Your data to store in the hash as JSON
	 */
	HashStorage.prototype.setHash = function(hash) {
		// If the data object is not an empty store it
		if(!this.isEmpty(hash)) {
			exports.location.hash = '#' + JSON.stringify(hash);
		}
	};
	
	/**
	 * Store data back into the hash object manually
	 * So when you need to update something without the user having to click something
	 * I.e. a counter or page scroll position, something programmatic
	 * 
	 * @param {Object} data Your custom data object to merge into the hash
	 */
	HashStorage.prototype.set = function(data) {
		this.merge(data, this.data);
		this.setHash(this.data);
	};
	
	/**
	 * Fetches and parses the hash, merges any hash changes into the data object
	 * Will be run every time the hash changes unless the event listener is removed
	 */
	HashStorage.prototype.parseHash = function() {
		// Initialise variables
		var hash = null;
		
		// Attempt to decode the hash
		hash = this.getHash();
		
		// Check for any existing data
		if(this.data) {
			// Check for a hash object
			// If there is some data merge it with the current object
			if(hash) {
				this.merge(hash, this.data);
			}
		}
		else {
			// There is nothing stored yet
			// Store either the hash or empty object
			this.data = hash || {};
		}
		
		// Because any merging has been completed we need to set the hash to the current data
		// This allows copying of the URL with the persistent hash based object
		this.setHash(this.data);
	};
	
	/**
	 * Merges one object into another
	 * Will recurse on objects to allow specific property setting but completely replace all other variable types
	 * 
	 * @param {Object} from Object containing the new properties to copy in
	 * @param {Object} to The base object to copy the properties into
	 */
	HashStorage.prototype.merge = function(from, to) {
		// Initialise variables
		var key = null;
		
		// Loop over the from object
		// Copy in anything that is not an object
		// If it is an object, recurse
		for(key in from) {
			if(from.hasOwnProperty(key)) {
				if(typeof from[key] !== 'object' || from[key] instanceof Array) {
					to[key] = from[key];
				}
				else {
					// If the destination does not contain the object initialise it
					if(typeof to[key] !== 'object') {
						to[key] = {};
					}
					
					// Recurse with the deeper object
					this.merge(from[key], to[key]);
				}
			}
		}
	};
	
	// Expose the class for AMD, CommonJS and browsers
	if(typeof define === 'function' && define.amd) {
		define(function() {
			return HashStorage;
		});
	}
	else if(typeof module === 'object') {
		module.exports = HashStorage;
	}
	else {
		exports.HashStorage = HashStorage;
	}
}(this));