﻿(function (window, $) {
	function pageState(stateKeys, options) {
		if (!(this instanceof pageState)) {
			return new pageState(stateKeys, options);
		}
		stateKeys = stateKeys || {};
		var aKeys = [], jWindow = $(window), oOldStateObj = jWindow.data('pageStateObj'), oStateObj = this;
		if (typeof oOldStateObj == 'object') {
			return oOldStateObj;
		} else {
			jWindow.data('pageStateObj', this);
		}
		this.optValues = $.extend(true, {}, this.defOptions, options);
		this.stateKeys = {};
		this.oState = {};
		for (var keyName in stateKeys) {
			if (stateKeys.hasOwnProperty(keyName)) {
				var keyVal = stateKeys[keyName], callBack, type;
				aKeys.push(keyName);
				if (typeof keyVal == 'function') {
					callBack = keyVal;
				} else if (typeof keyVal.func == 'function' || keyVal.func instanceof Array) {
					callBack = keyVal.func;
				}
				this.stateKeys[keyName] = {
					func: callBack || null,
					type: typeof keyVal.type != 'undefined' && keyVal.type == 'array' ? keyVal.type : 'string',
					deferred: keyVal.deferred
				};
			}
		}
		if (aKeys.length) {
			this.getState();
			this.execCallbacks();
			$(function () {
				oStateObj.onDomReady();
			})
		}
	}
	
	pageState.prototype = {
		defOptions: {
			pushFormat: '{$key}{$val}',
			hashStart: '#',
			keysAliases: {}
		},
		getState: function (oKeys, uri) {
			uri = uri || window.location.hash;
			oKeys = oKeys || {};
			var oUriVars = this.getVarsFromUri(uri);
			if (oUriVars)
			$.extend(true, this.stateKeys, oKeys);
			for (var keyName in this.stateKeys) {
				if (typeof oUriVars[keyName] != 'undefined') {
					this.addToState(keyName, oUriVars[keyName], this.stateKeys.type != 'array' ? 'replace' : true);
				}
			}
		},
		getStateKeys: function () {
			var oKeys = arguments[0], oResult = {}, oKeysFrom,
				stringKey = typeof oKeys == 'string', //первый аргумент может быть строкой, массивом или объектом
				keysArray = oKeys instanceof Array,
				keysObject = typeof oKeys == 'object';
			if (stringKey) {
				oKeys = {};
				oKeys[arguments[0]] = null;
			} else if (keysArray) {
				oKeys = {};
				for (var keyNo = 0, keysLength = arguments[0].length; keyNo < keysLength; keyNo += 1) {
					oKeys[arguments[0][keyNo]] = null;
				}
			} else if (!keysObject) {
				oKeys = {};
			}
			this.getState(oKeys);
			if (stringKey) {
				return this.oState[arguments[0]];
			} else if (keysObject) {
				oKeysFrom = oKeys;
			} else {
				oKeysFrom = this.oState;
			}
			for (var keyName in oKeysFrom) {
				if (oKeysFrom.hasOwnProperty(keyName)) {
					oResult[keyName] = this.oState[keyName];
				}
			}
			return oResult;
		},
		pushState: function (hashFormat, oParams) {
			hashFormat = hashFormat || this.optValues.pushFormat;
			var aHashBang = [], keyValString, keyVal, oStateParams;
			if (typeof oParams == 'object') {
				oStateParams = oParams;
			} else {
				oStateParams = this.oState;
			}
			for (var keyName in oStateParams) {
				if (oStateParams.hasOwnProperty(keyName)) {
					if (typeof this.oState[keyName] != 'undefined') {
						keyVal = this.oState[keyName];
					} else {
						keyVal = oStateParams[keyName];
					}
					if (keyVal === false) {
						continue;
					}
					if (keyVal instanceof Array) {
						keyVal = keyVal.join(',');
					}
					if (keyVal) {
						keyValString = hashFormat.replace('{$key}', keyName).replace('{$val}', keyVal);
					} else {
						keyValString = keyName;
					}
					aHashBang.push(keyValString);
				}
			}
			if (aHashBang.length) {
				window.location.hash = this.optValues.hashStart + aHashBang.join('&');
			} else if (window.location.hash.length) {
				window.location.hash = '!';
			}
		},
		execCallbacks: function () {
			var aKeys = arguments[0];
			this.Deferred = {};
			if (typeof aKeys != 'undefined') {
				if (typeof aKeys == 'string') {
					aKeys = [aKeys];
				}
			}
			if (aKeys instanceof Array) {
				for (var keyNo = 0, keysLength = aKeys.length; keyNo < keysLength; keyNo += 1) {
					this.execCollback(aKeys[keyNo]);
				}
			} else {
				for (var keyName in this.stateKeys) {
					if (this.stateKeys.hasOwnProperty(keyName)) {
						this.execCollback(keyName);
					}
				}
			}
		},
		execCollback: function (keyName) {
			var oKey = this.stateKeys[keyName];
			if (typeof oKey == 'undefined') {
				return;
			}
			if (typeof oKey.func == 'function') {
				this.chkDeffered(oKey, keyName);
			} else if (oKey.func instanceof Array) {
				for (var cbkNo = 0, cbksLength = oKey.func.length; cbkNo < cbksLength; cbkNo += 1) {
					if (typeof oKey.func[cbkNo].func == 'function') {
						this.chkDeffered(oKey.func[cbkNo], keyName);
					} else if (typeof oKey.func[cbkNo] == 'function') {
						this.chkDeffered({func: oKey.func[cbkNo]}, keyName);
					}
				}
			}
		},
		chkDeffered: function (oFunc, keyName) {
			if (!oFunc.deferred) {
				if (typeof this.oState[keyName] !=='undefined') {
					oFunc.func.call(this, this.oState[keyName]);
				}
			} else {
				this.Deferred[keyName] = oFunc.func;
			}
		},
		onDomReady: function () {
			for (var keyName in this.Deferred) {
				if (typeof this.Deferred[keyName] == 'function' && typeof this.oState[keyName] !=='undefined') {
					this.Deferred[keyName].call(this, this.oState[keyName]);
				}
			}
		},
		getVarsFromUri: function (uri, needVars) {
			var urlRegExp = /([^=\?&#!0-9]+)([0-9]+)?(?:=([^=\?&#!]+))?/gi, Vars = {}, aVrbl, hasVars = false,
				varName, varVal;
			if (!needVars || !(needVars instanceof Array)) {
				needVars = false;
			}
			if (!uri) {
				return false;
			}
			//uri = uri || document.location.href;
			while (aVrbl = urlRegExp.exec(uri)) {
				varName = aVrbl[1];
				if (typeof aVrbl[3] == 'undefined' || !aVrbl[3].length) {
					varVal = aVrbl[2];
				} else {
					if (typeof aVrbl[2] != 'undefined' && aVrbl[2].length) {
						varName += aVrbl[2];
					}
					varVal = aVrbl[3];
				}
				varVal = varVal || null;
				if (typeof varVal == 'string') {
					try {
						varVal = decodeURIComponent(varVal);
					} catch (ex) {}
					if (varVal.indexOf(',') != -1) {
						varVal = varVal.split(',');	
					}
				}
				if (typeof Vars[varName] == 'undefined') {
					Vars[varName] = varVal;
				} else {
					if (Vars[varName] instanceof Array) {
						Vars[varName].push(varVal);
					} else {
						Vars[varName] = [Vars[varName], varVal];
					}
				}
			}
			if (needVars) {
				for (var varNo = 0, newVars = {}, varsLength = needVars.length; varNo < varsLength; varNo++) {
					if (Vars[needVars[varNo]]) {
						newVars[needVars[varNo]] = Vars[needVars[varNo]];
					}
				}
				Vars = newVars;
			}
			for (var prop in Vars) {
				if (Vars.hasOwnProperty(prop)) {
					hasVars = true;
					break;
				}
			}
			return hasVars ? Vars : false;
		},
		addToParams: function (params, param, value, add) { //params Object, param String, [value Any], [add Boolean | 'replace']
			var hasParam = typeof params[param] != 'undefined',
				hasParamArray = params[param] instanceof Array, key;
			if (typeof add == 'undefined' || add == 'replace') {
				if (typeof value == 'undefined' || value === false) {
					add = false;
				} else {
					add = add || true;
				}
			}
			if (add) { //TODO: : честное добавление и удаление значений, если 3-й аргумент - массив
				if (hasParam && add != 'replace') {
					if (hasParamArray) {
						key = $.inArray(value, params[param]);
						if (key == -1) {
							params[param] = params[param].concat(value);
						}
					} else {
						params[param] = [params[param]].concat(value);
					}
				} else {
					params[param] = value;
				}
			} else {
				if (hasParam) {
					if (hasParamArray) {
						key = $.inArray(value, params[param]);
						if (key != -1) {
							params[param].splice(key, 1);
						}
					} else {
						params[param] = add;
					}
				}
			}
		},
		addToState: function (param, value, add, checkAliases) {
			var paramAliases = this.optValues.keysAliases[param];
			if (typeof checkAliases == 'undefined') {
				checkAliases = true;
			}
			if (checkAliases && typeof paramAliases != 'undefined') {
				if (typeof paramAliases == 'string') {
					this.addToParams(this.oState, paramAliases, false);
				} else if (paramAliases instanceof Array) {
					for (var alNo = 0, alsLength = paramAliases.length; alNo < alsLength; alNo += 1) {
						this.addToParams(this.oState, paramAliases[alNo], false);
					}
				}
			}
			this.addToParams(this.oState, param, value, add);
		},
		pushToState: function (param, value, add, format) {//первый параметр может представлять объект, тогда второй опускается
			var oParams;
			if (typeof arguments[0] != 'object') {
				oParams = {};
				oParams[arguments[0]] = arguments[1];
			} else {
				oParams = arguments[0];
				add = arguments[1];
				format = arguments[2];
			}
			for (var paramName in oParams) {
				if (oParams.hasOwnProperty(paramName)){
					this.addToState(paramName, oParams[paramName], add);
					if (typeof oParams[paramName] == 'undefined' || oParams[paramName] === false) {
						delete oParams[paramName];
					}
				}
			}
			oParams = $.extend({}, this.getVarsFromUri(window.location.hash), oParams); //TODO перенести в pushState, удалять из хэш, то что собираемся добавить
			this.pushState(format, oParams);
		}
	};
	
	$.initPageStateCntrlr = pageState;
	
	/*pageState({
		'test': function () {
			console.log(arguments);
		},
		'lala': function () {
			$(function () {
				console.log($('body'));
			});
		}
	});*/
})(self, jQuery);
