

function getEl(elId) 
{
	return document.getElementById(elId);
}

function getElByClass(className) 
{
	return document.getElementsByClassName(className)[0];
}

function getAllElsByClass(className) 
{
	return document.getElementsByClassName(className);
}

function isIterable(obj) 
{
	// checks for null and undefined
	if (obj == null) {
		return false;
	}
	return typeof obj[Symbol.iterator] === 'function';
}

function applyForEveryElOnList(elsList, _function) 
{
	for(var i = 0; i < elsList.length; i++) 
	{
		elsList[i] = _function(elsList[i], i);
	}
	
	return elsList;
}

function performOnElsList(elsList, _function, _resultHandler) 
{
	var result;
	var left = elsList.length;
	
	for(var i = 0; i < elsList.length; i++, left--) 
	{
		var eachRes = _function(elsList[i], i, left-1);
		
		if(false === eachRes) {
			break;
		}

		if(_resultHandler) {
			result = _resultHandler(eachRes, result);
		}
	}
	
	return result;
}

function performOnEveryKey(obj, _function, _resultHandler) 
{
	var result;
	var i = 0;
	
	for (var key in obj) 
	{
		if (obj.hasOwnProperty(key)) 
		{
			var eachRes = _function(key, obj[key], i);
			
			if(false === eachRes) {
				break;
			}
			
			if(_resultHandler) {
				result = _resultHandler(eachRes, result);
			}
			
			i++;
		}
	}
	
	return result;
}

function perfromRecursivelyOnList(list, startIndex, stepFunc, allDoneFunc) 
{
	stepFunc(list[startIndex], startIndex, function() {
		startIndex++;
		if(startIndex < list.length) {
			perfromRecursivelyOnList(list, startIndex, stepFunc, allDoneFunc);
		}
		else 
		{
			allDoneFunc();
		}
	});
}

function copyProps(to, source) 
{
	performOnEveryKey(source, function(key, val) {
		to[key] = val;
	});
	
	return to;
}

function cloneObj(obj) 
{
	return JSON.parse(JSON.stringify(obj));
}

function obj2arr(obj) 
{
	var arr = [];
	
	performOnEveryKey(obj, function(key, val) 
	{
		arr.push({
			key: key,
			value: val
		});
	});
	
	return arr;
}

//perfromRecursivelyOnList(["a", "b", "c"], 0, function(item, index, callback) 
//{
//	setTimeout(function() {
//		Debug.log("Processing item: " + item);
//		callback();
//	}, 1000);
//},
//function() 
//{
//	Debug.log("All items are processed.");
//});

function removeFromArray(array, el) 
{
	var index = array.indexOf(el);
	if (index > -1) 
	{
		array.splice(index, 1);
	}
}

function removeFromArrayOnCondition(array, func) 
{
	for(var i = 0; i < array.length; i++) 
	{
		if(func(array[i], i)) {
			array.splice(i, 1);
		}
	}
}

function arrayCondition(array, func) 
{
	for(var i = 0; i < array.length; i++) 
	{
		if(func(array[i], i)) {
			return true;
		}
	}
	
	return false;
}

function getBool(val)
{
	if(val === undefined || val === null) 
		return false;
	
	if(val === true || val === false)
		return val;
	
	switch (val.toString().toLowerCase().trim()) {
		case "true":
		case "yes":
		case "1":
		case "on":
			return true;

		case "false":
		case "no":
		case "0":
		case null:
			return false;

		default:
			return Boolean(val);
	}
}

function getString(val) 
{
	if(val) 
	{
		return val;
	}
	else 
	{
		return "";
	}
}

function escapeHtml(text)
{
	var map = {
		'&': '&amp;',
		'<': '&lt;',
		'>': '&gt;',
		'"': '&quot;',
		"'": '&#039;'
	};

	return text.replace(/[&<>"']/g, function (m) {
		return map[m];
	});
}

function moveCaretToEnd(el)
{
	if (typeof el.selectionStart == "number") {
		el.selectionStart = el.selectionEnd = el.value.length;
	} else if (typeof el.createTextRange != "undefined") {
		el.focus();
		var range = el.createTextRange();
		range.collapse(false);
		range.select();
	}
}

function OX(num) {
	return (num < 10) ? "0" + num : num;
}

function merge_options(obj1, obj2) {
	var obj3 = {};
	for (var attrname in obj1) {
		obj3[attrname] = obj1[attrname];
	}
	for (var attrname in obj2) {
		obj3[attrname] = obj2[attrname];
	}
	return obj3;
}



function setViewPortWidth(width)
{
	var metaTagsList = document.getElementsByTagName("meta");

	var viewPortSettings = metaTagsList["viewport"].getAttribute("content").split(",");

	for (var i = 0; i < viewPortSettings.length; i++)
	{
		var setting = viewPortSettings[i]
		if (setting.indexOf("width=") !== -1) {
			viewPortSettings[i] = "width=" + width;
		}
	}

	metaTagsList["viewport"].setAttribute("content", viewPortSettings.join(", "));
}

/***
   Copyright 2013 Teun Duynstee
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
     http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 */
var firstBy = (function() {
    function makeCompareFunction(f, direction){
      if(typeof(f)!="function"){
        var prop = f;
        // make unary function
        f = function(v1){return v1[prop];}
      }
      if(f.length === 1) {
        // f is a unary function mapping a single item to its sort score
        var uf = f;
        f = function(v1,v2) {return uf(v1) < uf(v2) ? -1 : uf(v1) > uf(v2) ? 1 : 0;}
      }
      if(direction === -1)return function(v1,v2){return -f(v1,v2)};
      return f;
    }
    /* mixin for the `thenBy` property */
    function extend(f, d) {
      f=makeCompareFunction(f, d);
      f.thenBy = tb;
      return f;
    }

    /* adds a secondary compare function to the target function (`this` context)
       which is applied in case the first one returns 0 (equal)
       returns a new compare function, which has a `thenBy` method as well */
    function tb(y, d) {
        var x = this;
        y = makeCompareFunction(y, d);
        return extend(function(a, b) {
            return x(a,b) || y(a,b);
        });
    }
    return extend;
})();

Array.prototype.firstNextToLast = function(amount) 
{
	var tmpUsers = [];
	var tmpLength = this.length;
	
	for(var i = 0; i < tmpLength; i++) 
	{
		if(i % 2 == 0 || ((amount !== undefined) && tmpUsers.length >= amount)) 
		{
			tmpUsers.push(this.shift());
		}
		else 
		{
			tmpUsers.push(this.pop());
		}
	}

	return tmpUsers;
};

Array.prototype.shuffle = function() 
{
  var i = this.length, j, temp;
  if ( i == 0 ) return this;
  while ( --i ) {
     j = Math.floor( Math.random() * ( i + 1 ) );
     temp = this[i];
     this[i] = this[j];
     this[j] = temp;
  }
  return this;
};

String.prototype.replaceAll = function(search, replacement) {
	var target = this;
	return target.replace(new RegExp(search, 'g'), replacement);
};

String.prototype.splitTrim = function(by) 
{
	return this.split(by).trim();
};

Array.prototype.trim = function(by) 
{
	return this.map(function(item) {
		return item.trim();
	});
};

function generateRundomString(len)
{
	var text = "";
	var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

	for (var i = 0; i <= len; i++) {
		text += possible.charAt(Math.floor(Math.random() * possible.length));
	}

	return text;
}

function isParentHasClass(child, className) 
{
	return (getParentByClass(child, className) != undefined);
}

function getParentByClass(child, className) 
{
	var node = child;
	while (node != null) {
		if (node.className && node.className.indexOf(className) > -1) {
			return node;
		}
		node = node.parentNode;
	}
	return undefined;
}

function isElHasParent(child, el) 
{
	var node = child;
	while (node != null) {
		if (node == el) {
			return true;
		}
		node = node.parentNode;
	}
	return false;
}

String.prototype.capitalizeFirstLetter = function() {
    return this.charAt(0).toUpperCase() + this.slice(1);
};

function isNumeric(val) 
{
	return parseFloat(val) == val;
}

var Debug = 
{
	options: {
		info: false,
		error: true
	},
	info: function(str) 
	{
		if(this.options.info) {
			console.log(str);
		}
	},
	error: function(str) 
	{
		if(this.options.error) {
			console.log(str);
		}
	}
};

function joinObject(object, glue, separator)
{
	if (glue === undefined) glue = '=';

	if (separator === undefined) separator = ',';

	var res = [];

	performOnEveryKey(object, function(key, val) {
		res.push(key + glue + val);
	});
	
	return res.join(separator);
}

function getUrlInfo(url) 
{
	var a = document.createElement('a');
	a.href = url;

	return {
		href: a.href,
		host: a.host,
		hostname: a.hostname,
		port: a.port,
		pathname: a.pathname,
		protocol: a.protocol,
		hash: a.hash,
		search: a.search
	};
}

var StringExt = 
{
	escapeRegExp: function (strToEscape) 
	{
		// Escape special characters for use in a regular expression
		return strToEscape.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
	},
	trimChar: function (origString, charToTrim) 
	{
		charToTrim = this.escapeRegExp(charToTrim);
		var regEx = new RegExp("^[" + charToTrim + "]+|[" + charToTrim + "]+$", "g");
		return origString.replace(regEx, "");
	}

};

function OR(a, b) 
{
	return (a !== null && a !== undefined) ? a : b;
}

if(!document.contains) 
{
	document.contains = function(el) 
	{
		while(el.parentNode)
			el = el.parentNode;
		
		return el === document;
	}
}

function parseVersionString(versionString) 
{
  return parseFloat(versionString);
}

var Storage = 
{
	storage: null,
	
	init: function(storage) 
	{
		this.storage = storage;
	},
	save: function(key, value) 
	{
		this.storage.save(key, value); 
	},
	get: function(key) 
	{
		return this.storage.get(key); 
	},
	remove: function(key) 
	{
		this.storage.remove(key); 
	}
};

var CookieStorage = 
{
	get: function (key) 
	{
		if (!key) {
			return null;
		}
		return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(key).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
	},
	save: function(key, value) 
	{
		return this._save(key, value, Infinity, "/")
	},
	_save: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {
		if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) {
			return false;
		}
		var sExpires = "";
		if (vEnd) {
			switch (vEnd.constructor) {
				case Number:
					sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd;
					break;
				case String:
					sExpires = "; expires=" + vEnd;
					break;
				case Date:
					sExpires = "; expires=" + vEnd.toUTCString();
					break;
			}
		}
		document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + "; domain=" + (sDomain ? sDomain : "."+document.domain) + (sPath ? "; path=" + sPath : "/") + (bSecure ? "; secure" : "");
		return true;
	},
	remove: function (key, sPath, sDomain) 
	{
		if (!this.has(key)) {
			return false;
		}
		
		var date = new Date();
		date.setTime(-1);
		
		document.cookie = encodeURIComponent(key) + "=; expires=" + date.toGMTString() + ";";
		return true;
	},
	has: function (sKey) {
		if (!sKey) {
			return false;
		}
		return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
	},
	keys: function () {
		var aKeys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, "").split(/\s*(?:\=[^;]*)?;\s*/);
		for (var nLen = aKeys.length, nIdx = 0; nIdx < nLen; nIdx++) {
			aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]);
		}
		return aKeys;
	}
};

var LocalStorage = 
{
	storage: window.localStorage,
	
	save: function(key, value) 
	{
		this.storage.setItem(key, value); 
	},
	get: function(key) 
	{
		return this.storage.getItem(key); 
	},
	remove: function(key) 
	{
		this.storage.removeItem(key); 
	}
};

var EventsManager = 
{
	listeners: [],
	
	_subscribersCounter: 0,
	
	history: {},
	
	subscribe: function(event, func, options)
	{
		options = merge_options({
			id: null,
			subtype: null,
			once: false,
			noQ: false
		}, options);
		
//		if(options.once && !options.id) {
//			Debug.error("EventsManager.subscribeOnce to event ["+event+"] require ID. ");
//		}
		
		Events.ifEventSupported(event, function() 
		{
			if(!EventsManager.listeners[event]) {
				EventsManager.listeners[event] = [];
			}
			
			if(!options.id) {
				options.id = EventsManager.generateId(event);
			}
			
			Debug.info(options.id);
			
			EventsManager.listeners[event][options.id] = 
			{
				func: function(data, event, subtype) 
				{
					if(!options.subtype || options.subtype == subtype) 
					{
						var res = func(data, event, subtype);
						
						if(options.once || res === false) {
							EventsManager.unsubscribe(event, options.id);
						}
					}
				},
				options: options
			};
		});
	},
	
	subscribeMulti: function(events, func, options) 
	{
		performOnElsList(events, function(event) {
			EventsManager.subscribe(event, func, options);
		});
	},
	
	generateId: function(event) {
		var id = 0;
		if(!this.listeners[event]) {
			this.listeners[event] = [];
		}
		while(this.listeners[event][event+id] !== undefined) {
			id++;
		}
		
		return event+id;
	},
	
	subscribeOnce: function(event, func, options) 
	{
		options = merge_options({
			once: true
		}, options);
		
		this.subscribe(event, func, options);
	},
	
	unsubscribe: function(event, id) 
	{
		Debug.info("Unsubscribe: " + id);
		
		if(this.listeners[event] && this.listeners[event][id]) 
		{
			delete(this.listeners[event][id]);
		}
	},
	
	unsubscribeAll: function(event) 
	{
		if(this.listeners[event]) {
			delete(this.listeners[event]);
			this.listeners[event] = {};
		}
	},
	
	unsubscribeEvery: function(eventsList) 
	{
		performOnElsList(eventsList, function(event) {
			EventsManager.unsubscribeAll(event);
		});
	},
	
	getListeners: function(event) 
	{
		var listeners = [];
		performOnEveryKey(EventsManager.listeners[event], function(key, handler) {
			listeners.push(handler);
		});
		
		return listeners;
	},
	
	fire: function(event, data, options) 
	{
		
		options = merge_options({
			noQ: false, // no queue
			subtype: ""
		}, options);
		
		Events.ifEventSupported(event, function() 
		{
			EventsManager.history[event] = OR(EventsManager.history[event], {});
			EventsManager.history[event][options.subtype] = {
				data: data,
				options: options
			};
			
			performOnElsList(EventsManager.getListeners(event), function(handler) 
			{
				if(options.noQ || handler.options.noQ) {
					handler.func(data, event, options.subtype);
				} else {
					Threader.putInQueue(function() {
						handler.func(data, event, options.subtype);
					});
				}
			});
		});
	},
	
	refire: function(event, subtype) 
	{
		if(!subtype) subtype = "";
		
		if(EventsManager.history[event] && EventsManager.history[event][subtype]) 
		{
			EventsManager.fire(event, EventsManager.history[event][subtype].data, EventsManager.history[event][subtype].options);
		}
	},
	wasFired: function(event, subtype) 
	{
		if(!subtype) subtype = "";
		
		return (EventsManager.history[event] && EventsManager.history[event][subtype]); 
	}
};

var Events = 
{
	registeredEvents: {},
	registerEvents: function(list) 
	{
		var namespace = null;
		performOnEveryKey(Events, function(key, val) {
			if(val === list) {
				namespace = key;
			}
		});
		
		if(namespace) {
			performOnEveryKey(list, function(key, val) 
			{
				list[key] = [namespace, key].join(".");
				Events.registeredEvents[list[key]] = [list[key]];
			});
		} else {
			throw "Cannot register events - no namespace found.";
		}
	},
	isEventSupported: function(event) 
	{
		return Events.registeredEvents[event] !== undefined;
	},
	
	ifEventSupported: function(event, func) 
	{
		if(Events.isEventSupported(event)) {
			func();
		} else {
			Debug.error("EventsManager: event with name '"+event+"' not supported.");
		}
	},
};

EventsManager.commands = 
{
	unsubscribe: "unsubscribe"
};

var Ajax = new function ()
{
	function objectToHttpParam(dataObject, _key)
	{
		var retStr = "";

		for (var key in dataObject)
		{
			var value = dataObject[key];

			if (_key)
			{
				key = _key + "[" + key + "]";
			}

			if (Object.prototype.toString.call(value) === '[object Array]')
			{
				for (var i = 0; i < value.length; i++)
				{
					var tmpValue = value[i];
					if (typeof tmpValue === 'string')
					{
						retStr += encodeURIComponent(key) + "[]=" + encodeURIComponent(tmpValue) + "&";
					}
					else
					{
						retStr += objectToHttpParam(tmpValue, key + "[" + i + "]");
					}

				}
			}
			else if (Object.prototype.toString.call(value) === '[object Object]')
			{
				retStr += objectToHttpParam(value, key);
			}
			else
			{
				retStr += encodeURIComponent(key) + "=" + encodeURIComponent(value) + "&";
			}
		}

		return retStr;
	}
	
	this.expectedStatuses = [];
	this.requestHeaders = [];
	
	function isSuccessStatus(xhr) 
	{
		return (xhr.status >= 200 && xhr.status < 300) || (Ajax.expectedStatuses.indexOf(xhr.status) != -1);
	}


	this.Invoke = function(params, callback, errorCallback)
	{
		var xmlhttp;
		var dataToSend = null;
		
		// create crossbrowser xmlHttpRequest
		if (window.XMLHttpRequest)
		{
			// code for IE7+, Firefox, Chrome, Opera, Safari
			xmlhttp = new window.XMLHttpRequest();
		}
		else
		{
			// code for IE6, IE5
			xmlhttp = new window.ActiveXObject("Microsoft.XMLHTTP");
		}
		
		xmlhttp.onreadystatechange = function()
		{
			if (xmlhttp.readyState == 4)
			{
				if(isSuccessStatus(xmlhttp)) {
					Ajax.onLoad(xmlhttp, params, callback, errorCallback);
				}
				else 
				{
					if(xmlhttp.status == 429) 
					{
						var retryAfter = xmlhttp.getResponseHeader('Retry-After');
						if(!retryAfter) retryAfter = 2000;
						
						Timeout.set(function() 
						{
							Ajax.Invoke(params, callback, errorCallback);
						}, retryAfter);
					}
					
					try 
					{
						EventsManager.fire(Events.XMLHTTP.requestFailed, {
							xmlhttp: xmlhttp
						}, {
							noQ: true
						});
						
						throw "Invalid response.";
						
					} catch (e) 
					{
						if(errorCallback) {
							errorCallback(e, xmlhttp);
						}
					}
				}
			}
		};
		
		xmlhttp.onabort = function() 
		{
			xmlhttp.aborted = true;
			
			Debug.info("Request was aborted: " + xmlhttp.userData.url
					  + "\nData: " + JSON.stringify(xmlhttp.userData.data));
		};
		
		xmlhttp.onloadend = function()
		{
			Debug.info("Request was loaded: " + xmlhttp.userData.url
					  + "\nData: " + JSON.stringify(xmlhttp.userData.data));
		};

		xmlhttp.userData = params;
		
		if(params.headers) 
		{
			performOnElsList(params.headers, function(header) {
				xmlhttp.setRequestHeader(header.key, header.value);	
			});
		}

		if (params.type && params.type.toString().toUpperCase() == 'POST')
		{
			xmlhttp.open(params.type, params.url, true);
			
			if(params.data) {
				dataToSend = objectToHttpParam(params.data);
				xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
			}

			if(params.formData) {
				dataToSend = params.formData;
				// don't set content type as it will miss 'boundary' value
				//xmlhttp.setRequestHeader("Content-type", "multipart/form-data");
			}
		}
		else
		{
			xmlhttp.open(params.type, params.url + objectToHttpParam(params.data), true);
		}
		
		EventsManager.fire(Events.XMLHTTP.beforeSend, {
			xmlhttp: xmlhttp
		}, {
			noQ: true
		});
		
		xmlhttp.send(dataToSend);
		
		return xmlhttp;
	};
	
	/**
	 * 
	 * @param {type} xmlhttp
	 * @param {AjaxRequest} params
	 * @param {type} callback
	 * @param {type} errorCallback
	 * @returns {undefined}
	 */
	this.onLoad = function(xmlhttp, params, callback, errorCallback) 
	{
		if(!xmlhttp.aborted) 
		{
			EventsManager.fire(Events.XMLHTTP.onLoad, {
				xmlhttp: xmlhttp
			}, {
				noQ: true
			});
			
			var response = xmlhttp.responseText;

			try 
			{
				if(params.processor) {
					response = params.processor(xmlhttp.responseText);
				}
				
				if(callback) {
					callback(response, xmlhttp.status);
				}
			}
			catch(e) 
			{
				Debug.error(e);
				Debug.error(xmlhttp);

				EventsManager.fire(Events.XMLHTTP.invalidResponse, {
					xmlhttp: xmlhttp,
					response: response
				}, {
					noQ: true
				});

				if(errorCallback) {
					errorCallback(e, xmlhttp);
				}
			}
		}
	};
	
	this.abourtRequest = function(request) 
	{
		request.aborted = true;
		request.abort();
		request = null;
		
		return request;
	};
};

var AjaxRequest = 
{
	url: null,			// 
	type: null,			// post, get
	data: null,			// 
	formData: null,	// 
	processor: null	// func
};

Events.XMLHTTP = 
{
	onLoad: "onLoad",
	beforeSend: "beforeSend",
	requestFailed: "requestFailed",
	invalidResponse: "invalidResponse",
};

Events.registerEvents(Events.XMLHTTP);

var Register = 
{
	
};

var Templater = 
{
	attrs: {
		action: "data-js-action",
		digested: "data-js-digested",
		
		preserveContent: "data-js-preserve-content",
		contentPreserved: "data-js-content-preserved",
		
		macrosApplied: function(key) {
			return "data-js-"+key+"-applied";
		}
	},
	
	preservedContent: [],
	
	init: function() 
	{
		Templater.GarbageCollector.init();
	},
	digest: function(el, data, redigest)
	{
		TemplatesManager.retrieveTemplates(el);
		Templater.MacrosManager.retrive(el);
		
		performOnElsList(el.querySelectorAll("["+Templater.attrs.preserveContent+"]"), function(el) 
		{
			if(!UIManager.elHassAttr(el, Templater.attrs.contentPreserved)) 
			{
				Templater.preservedContent.push({
					el: el,
					content: UIManager.getHTML(el)
				});
				UIManager.clearEl(el);
				
				UIManager.setElAttr(el, Templater.attrs.contentPreserved, true);
			}
		});
		
		performOnEveryKey(Templater.MacrosManager.available, function(key, val) 
		{
			performOnElsList(el.querySelectorAll("["+val.attr+"]"), function(el) 
			{
				if(redigest || false == UIManager.elHassAttr(el, Templater.attrs.macrosApplied(key)))
				{
					/**
					 * 
					 * @type {Templater.Macros}
					 */
					var macros = Templater.MacrosManager.available[key];

					performOnElsList(macros.actions, function(actionInfo) 
					{
						/**
						 * 
						 * @type {Templater.Action}
						 */
						var action = new Templater.Action(actionInfo.name, el, data, actionInfo.alias, macros.el, {
							macros: {
								value: UIManager.getElAttr(el, val.attr)
							}
						});
						
						Templater.runAction(action);
					});
					
					UIManager.setElAttr(el, Templater.attrs.macrosApplied(key), true);
				}
			});
		});

		performOnElsList(el.querySelectorAll("["+Templater.attrs.action+"]"), function(el) 
		{
			if(!UIManager.getElAttr(el, Templater.attrs.digested) ) 
			{
				UIManager.setElAttr(el, Templater.attrs.digested, true);
				
				Templater.processActions(UIManager.getElAttr(el, Templater.attrs.action), el, data);
			}
		});
		
		EventsManager.fire(Events.Templater.elementDigested, {
			el: el
		});
		
		EventsManager.fire(Events.Templater.garbageCollectorDeactivateZombies);
	},
	processActions: function(actionsStr, el, data, parent) 
	{
		var actionsList = Templater.ActionsParser.retriveActions(actionsStr, el, data, parent);
		
		performOnElsList(actionsList, function(action) 
		{
			Templater.runAction(action);
		});
	},
	/**
	 * 
	 * @param {Templater.Action} action
	 * @returns {undefined}
	 */
	runAction: function(action) 
	{
		if(!Templater.actions[action.action]) {
			Debug.error("Action [" +action.action+ "] is not supported. ");
		}
		
		Templater.GarbageCollector.addAction(action);

		Templater.performPreAction(action);

		if(action.wait) 
		{
			Templater.wait._process(action);
		}
		else 
		{
			Templater.perform(action);
		}
	},
	/**
	 * 
	 * @param {Templater.Action} action
	 * @returns {undefined}
	 */
	performPreAction: function(action) 
	{
		if(Templater.actions[action.action]._pre) {
			Templater.actions[action.action]._pre(action);
		}
	},
	/**
	 * 
	 * @param {Templater.Action} action
	 * @param {type} el
	 * @param {type} data
	 * @returns {undefined}
	 */
	perform: function(action) 
	{
		if(getBool(action.getProp("debug")) == true) {
			debugger;
		}
		// HINT: checking if elemnt is still in the DOM
		// and if it's not then there's no reason we should perform the action.
		if(document.contains(action.el)) 
		{
			action.prepare();
		
			if(!action.if || getBool(new Temparser(null, action.getData(), null).evaluateCondition(action.if)))
			{
				EventsManager.fire(Events.Templater.performingAction, {
					action: action
				});

				if (action.before) {
					Templater.processActions(action.before, action.el, action.getData(), action.parentEl);
				}

				var result = Templater.actions[action.action].perform(action);

				if(Templater.actions[action.action]._post) {
					Templater.actions[action.action]._post(action, result);
				}

				EventsManager.fire(Events.Templater.actionPerfromed, {
					action: action
				});

				if (action.after) {
					Templater.processActions(action.after, action.el, action.getData(), action.parentEl);
				}
			}
			else if(action.else) 
			{
				Templater.processActions(action.else, action.el, action.getData(), action.parentEl);
			}
			
			if(!action.awaits) {	
				action.active = false;
			}
		}
		else 
		{
			action.valid = false;
		}
		
		EventsManager.fire(Events.Templater.garbageCollectorCleanUp);
	},
	wait: 
	{
		/**
		 * 
		 * @param {Templater.Action} action
		 * @returns {undefined}
		 */
		_process: function(action) 
		{
			performOnElsList(Templater.ActionsParser.retriveNameWithAlliases(action.wait), function(wait) 
			{
				if(Templater.wait[wait.name]) 
				{
					Templater.wait[wait.name](action, wait.alias);
				}
				else 
				{
					Debug.error("When type [" +wait.name+ "] is not supported. ");
				}
			});
		},
		/**
		* 
		* @param {Templater.Action} action
		* @returns {undefined}
		*/
		now: function(action, allias) 
		{
			Templater.perform(action);
		},
		/**
		 * 
		 * @param {Templater.Action} action
		 * @param {type} alias
		 * @returns {Templater.wait.event}
		 */
		event: function (action, alias)
		{
			var _this = this;
			
			var eventsList = action.getProp(alias+"-when").splitTrim(",");
			
			/**
			 * 
			 * @param {type} event
			 * @param {Templater.Action} action
			 * @param {type} alias
			 * @returns {undefined}
			 */
			this.subscribe = function(event, action, alias) 
			{
				var args = {
					repeat: action.getProp(alias+"-repeat", 'repeat'),
					subtype: action.getProp(alias+"-subtype")
				};
				
				EventsManager.subscribe(event, function (data, event, subtype)
				{
					action.setData("event", data);
					action.setData("_event", {
						name: event,
						subtype: subtype
					});
					
					action.awaits = (args.repeat === 'repeat');

					Templater.perform(action);

					if(action.valid && action.awaits) {
						_this.subscribe(event, action, alias);
					}

				}, {
					subtype: args.subtype,
					once: true
				});
			};
			
			performOnElsList(eventsList, function(event) {
				_this.subscribe(event, action, alias);
			});
		},
		/**
		 * 
		 * @param {Templater.Action} action
		 * @param {type} alias
		 * @returns {undefined}
		 */
		trigger: function(action, alias) 
		{
			var args = {
				repeat: action.getProp(alias+"-repeat", 'repeat'),
			};	
			
			EventsManager.subscribe(Events.Templater.triggerAction, function (data, event, subtype)
			{
				action.setData("event", data.data);
				action.setData("trigger", data.action);
				action.setData("_event", {
					name: event,
					subtype: subtype
				});
				
				action.awaits = (args.repeat === 'repeat');
				
				Templater.perform(action);
				
				if(action.valid && action.awaits) {
					Templater.wait.trigger(action, alias);
				}
			}, 
			{
				subtype: action.id,
				once: true
			});
		},
		/**
		 * 
		 * @param {Templater.Action} action
		 * @param {type} alias
		 * @returns {undefined}
		 */
		user: function(action, alias) 
		{
			var args = {
				when: action.getProp(alias+"-when"),
				noDef: action.getProp(alias+"-noDef"),
				confirm: action.getProp(alias+"-confirm"),
				debounceSec: parseFloat(action.getProp(alias+"-debounce", '0')),
			};

			var debouncer = null;
			
			UIManager.addEvent(action.el, args.when, function (e)
			{
        if(args.noDef) {
          e.preventDefault();
        }

			  var callback = function()
        {
          action.setData("_event", e);

          if(args.confirm)
          {
            PageAlerts.confirm(args.confirm, function() {
              Templater.perform(action);
            });
          }
          else
          {
            Templater.perform(action);
          }
        };

			  if(args.debounceSec > 0)
			  {
          debouncer = Timeout.debounce(callback, args.debounceSec * 1000, debouncer);
        }
        else
        {
          callback();
        }

			});
		},
		/**
		 * 
		 * @param {Templater.Action} action
		 * @param {type} el
		 * @returns {undefined}
		 */
		timeout: function (action, alias)
		{
			Timeout.set(function ()
			{
				Templater.perform(action);

			}, action.getProp(alias));
		}
	}
};

/**
 * 
 * @param {type} action
 * @param {type} el
 * @param {type} data
 * @param {type} alias
 * @param {Templater.Action} parentEl
 * @returns {Templater.Action}
 */
Templater.Action = function(action, el, data, alias, parentEl, contexts)
{
	var _this = this;
	
	// HINT: becomes invalid when element is no longer in DOM
	this.valid = true;
	this.active = true;
	this.awaits = false;
	
	this.el = el;
	this.action = action;
	this.alias = OR(alias, action);
	this.parentEl = parentEl;
	
	this.prepare = function() 
	{
		if(this.parentEl) {
			this._retrieveDataAttrbs(this.parentEl);
		}
		
		this._retrieveDataAttrbs(this.el);
		
		this.setData("_global", Templater.Glob.data);
		this.setData("_elContent", UIManager.getHTML(el).trim());
	};
	
	this._retrieveDataAttrbs = function(el) 
	{
		var dataAttr = "data-js-"+this.alias.toLowerCase()+"-data-";
		
		performOnElsList(el.attributes, function(attr) {
			if(attr.name.indexOf(dataAttr) === 0) 
			{
				var prop = attr.name.replace(dataAttr, "").split("-")[0];
				_this.data[prop] = _this.getProp("data-"+prop);
			}
		});
	};
	
	this.getProp = function(prop, _default) 
	{
		prop = prop.toLowerCase();
		
		var result = this._retrievePropFromEl(this.el, prop);

		if(result === null && this.parentEl) 
		{
			result = this._retrievePropFromEl(this.parentEl, prop);
		}
		
		return OR(result, _default);
	};
	
	this._retrievePropFromEl = function(el, prop) 
	{
		var result = null;
		
		var attr = "js-"+this.alias+"-"+prop;
		
		var expr = UIManager.getElData(el, attr+"-expr");
		
		if(expr) 
		{
			result = new Temparser(null, this.getData()).evaluate(expr);
		}
		else if(UIManager.elHasData(el, attr)) 
		{
			result = UIManager.getElData(el, attr);
		}
		
		return result;
	};
	
	this.getData = function(context) 
	{
		return new Temparser(null, this.data).parseValue(OR(context, this.context));
	};
	
	this.setData = function(context, data) 
	{
		this.data[context] = data;
	};
	
	this.data = {
		data: OR(data, {}),
		action: _this,
	};
	
	if(contexts) {
		performOnEveryKey(contexts, function(key, val) {
			_this.data[key] = val;
		});
	}
	
	this.context = this.getProp('context', "data");
	
	this.id = this.getProp("id", this.action);
	this.wait = this.getProp("wait");
	
	this.if = this.getProp("if");
	this.else = this.getProp("else");
	
	this.before = this.getProp("before");
	this.after = this.getProp("after");
};

Templater.ActionsParser = 
{
	retriveActions: function(actionsStr, el, data, parent) 
	{
		var actions = [];
		
		performOnElsList(this.retriveNameWithAlliases(actionsStr), function(action) 
		{
			actions.push(new Templater.Action(action.name, el, data, action.alias, parent));
		});
		
		return actions;
	},
	
	retriveNameWithAlliases: function(actionsStr) 
	{
		var result = [];
		
		var actionsStrList = actionsStr.splitTrim(",");
		
		performOnElsList(actionsStrList, function(item) 
		{
			var name = item;
			var alias = name;
			
			var valSplit = item.split("=");
			if(valSplit.length > 1) {
				alias = valSplit[0];
				name = valSplit[1];
			}
			
			result.push({
				name: name,
				alias: alias
			});;
		});
		
		return result;
	}
};

Templater.Glob = 
{
	data: {
		
	},
	get: function(action, key, _default) 
	{
		var result = _default;
		if(this.data[action]) 
		{
			result = this.data[action];
			if(key) {
				result = this.data[action][key];
			}
		} else {
			// if wasn't found - setting default
			if(key) {
				this.set(action, key, result);
			} else {
				this.data[action] = result;
			}
		}
		
		return result;
	},
	set: function(action, key, val) 
	{
		if(!this.data[action]) {
			this.data[action] = {};
		}
		this.data[action][key] = val;
	}
};

Templater.GarbageCollector = 
{
	activeActions: [],
	
	init: function() 
	{
		EventsManager.subscribe(Events.Templater.garbageCollectorDeactivateZombies, function() 
		{
			Templater.GarbageCollector.deactivateZombies();
		});
		
		EventsManager.subscribe(Events.Templater.garbageCollectorCleanUp, function() 
		{
			Templater.GarbageCollector.collectGarbage();
		});
	},
	
	addAction: function(action) 
	{
		this.activeActions.push(action);
	},
	
	deactivateZombies: function() 
	{
		performOnElsList(this.activeActions, function(action, i) 
		{
			if(document.contains(action.el) == false) {
				action.valid = false;
				action.active = false;
			}
		});
	},
	
	collectGarbage: function() 
	{
		var i = this.activeActions.length;
		
		while (i-- && i !== -1) 
		{
			if(this.activeActions[i].active === false) {
				this.activeActions.splice(i, 1);
			}
		}
	}
};


Events.Templater = 
{
	triggerAction: "triggerAction",
	elementDigested: "elementDigested",
	performingAction: "performingAction",
	actionPerfromed: "actionPerfromed",
	
	garbageCollectorDeactivateZombies: "garbageCollectorDeactivateZombies",
	garbageCollectorCleanUp: "garbageCollectorCleanUp"
};

Events.registerEvents(Events.Templater);

Templater.actions = 
{
	void: 
	{
		perform: function() 
		{
			return;
		}
	},
	trigger: 
	{
		perform: function(action) 
		{
			performOnElsList(action.getProp("action", "").splitTrim(","), function(action_id) 
			{
				EventsManager.fire(Events.Templater.triggerAction, 
				{
					data: action.getData(),
					action: action
				}, 
				{
					noQ: true,
					subtype: action_id
				});
			});
		}
	},
	retrigger: 
	{
		perform: function(action) 
		{
			performOnElsList(action.getProp("action", "").splitTrim(","), function(action_id) 
			{
				EventsManager.refire(Events.Templater.triggerAction, action_id);
			});
		}
	},
	execute: 
	{
		/**
		 * 
		 * @param {Templater.Action} action
		 * @returns {unresolved}
		 */
		perform: function(action) 
		{
			var code = action.getProp("func", "");
			
			performOnElsList(code.splitTrim(";"), function(piece) 
			{
				new Temparser(null, action.getData(), null).evaluateExpression(piece);
			});
		}
	},
	fire: 
	{
		perform: function(action) 
		{
			return EventsManager.fire(action.getProp("event"), action.getProp("data"), {
				noQ: true,
				subtype: action.getProp("subtype")
			}); 
		}
	},
	condition: 
	{
		/**
		 * 
		 * @param {Templater.Action} action
		 * @returns {undefined}
		 */
		perform: function(action) 
		{
			var cond = action.getProp("expr", "");
			var when_true = action.getProp("true");
			var when_false = action.getProp("false");

			if(getBool(new Temparser(null, action.getData(), null).evaluateCondition(cond))) 
			{
				if(when_true) {
					Templater.processActions(when_true, action.el, action.getData(), action.parentEl);
				}
			}
			else 
			{
				if(when_false) {
					Templater.processActions(when_false, action.el, action.getData(), action.parentEl);
				}
			}
		}
	},
	/**
	 * 
	 * @param {Templater.Action} action
	 * @param {type} el
	 * @returns {undefined}
	 */
	include: 
	{
		perform: function(action) 
		{
			UIManager.setHTML(action.el, TemplatesManager.getHTML(action.getProp("template")));
			
			Templater.digest(action.el, action.getData());
		}
	},
	digest:
	{
    perform: function(action)
    {
      Templater.digest(action.el, action.getData(), true);
    }
	},
	/**
	 * 
	 * @param {Templater.Action} action
	 * @param {type} el
	 * @returns {undefined}
	 */
	draw: 
	{
		_pre: function(action) 
		{
			action.html = UIManager.getHTML(action.el);
			
			var placeholder = action.getProp('placeholder');

			if(placeholder) {
				UIManager.setHTML(action.el, placeholder);
			}
		},
		perform: function(action) 
		{
			return new Temparser(Templater.actions.draw.__getContent(action), action.getData(), null).format();
		},
		_post: function(action, result) 
		{
			if(getBool(action.getProp("clear", true))) {
				UIManager.clearEl(action.el);
			}
			
			var isAbove = action.getProp("above", false);

			UIManager.addNodeFromHTML(action.el, result, isAbove);			
			Templater.digest(action.el, action.getData());
		},
		__getContent: function(action) 
		{
			if(action.getProp("template")) {
				return TemplatesManager.getHTML(action.getProp("template"));
			}
			
			if(action.getProp("page")) {
				return AppPage.getPageHTML(action.getProp("page"));
			}
			
			if(action.getProp("content")) {
				return action.getProp("content");
			}
			
			var content = action.html;
			performOnElsList(Templater.preservedContent, function(item) {
				if(item.el === action.el) {
					content = item.content;
					return false;
				}
			});

			return content;
		}
	},
	drawEvery:
	{
		_pre: function(action) 
		{
			return Templater.actions.draw._pre(action);
		},
		perform: function(action) 
		{
			var args = 
			{
				clear: getBool(action.getProp("clear", true)),
				alias: action.getProp("alias", "val"),
				sep: action.getProp("sep", ""),
				sepEvery: parseInt(action.getProp("sepEvery", 1)),
				isAbove: action.getProp("above", false),
				
				every: action.getProp('every'),
				everyIf: action.getProp("everyIf", false),
				everyAfter: action.getProp("everyAfter", false),
				
				ifNone: action.getProp('ifNone')
			};
			
			var every = new Temparser(null, action.getData()).evaluateExpression(args.every);	

			if(every.length > 0) 
			{
				if(args.clear) {
					UIManager.clearEl(action.el);
				}
				
				return performOnElsList(every, 
				function(val, i, l) 
				{
					var itemData = {
						parent: action.getData(),
						val: val,
						index: i
					};
					
					itemData[args.alias] = val;
					
					action.setData("_item", itemData);
					
					if(!args.everyIf || getBool(new Temparser(null, itemData, null).evaluateCondition(args.everyIf))) 
					{
						// if not first element and the amount of elements that needs to be separated is reached
						var sep = (l > 0 && ((i+1) % args.sepEvery == 0)) 
									? args.sep 
									: "";

						var html = new Temparser(Templater.actions.draw.__getContent(action), itemData, null).format();

						Threader.run(function() 
						{
							UIManager.addNodeFromHTML(action.el, html+sep, args.isAbove);

							Templater.digest(action.el, itemData);

							if (args.everyAfter) {
								Templater.processActions(args.everyAfter, action.el, itemData, action.parentEl);
							}
						}, 
						{
							inQ: false,
							callback: null
						});
					}
				},
				function(eachRes, result) 
				{
					return OR(result, "") + eachRes;
				});
			}
			else 
			{
				if(args.ifNone) {
					UIManager.setHTML(action.el, args.ifNone);
				}
			}
		}
	},
	setHTML: 
	{
		perform: function(action) 
		{
			UIManager.setHTML(action.el, action.getProp("value"));
		}
	},
	addHTML: 
	{
		perform: function(action) 
		{
			UIManager.addNodeFromHTML(action.el, action.getProp("value"), action.getProp("above"));
		}
	},
	clearHTML: 
	{
		perform: function(action) 
		{
			UIManager.clearEl(action.el, action.getProp("value"));
		}
	},
  copyHTML:
	{
		perform: function(action)
		{
      UIManager.setHTML(action.el, UIManager.getHTML(getEl(action.getProp("from"))));
		}
	},
	show: 
	{
		perform: function(action) 
		{
			UIManager.showEl(action.el, action.getProp("display"));
		}
	},
	hide: 
	{
		perform: function(action) 
		{
			UIManager.hideEl(action.el);
		}
	},
	enable: 
	{
		perform: function(action) 
		{
			UIManager.enEl(action.el);
		}
	},
	disable: 
	{
		perform: function(action) 
		{
			UIManager.disEl(action.el);
		}
	},
	focus: 
	{
		perform: function(action) 
		{
			UIManager.setFocus(action.el);
		}
	},
	placeholder: 
	{
		perform: function(action) 
		{
			UIManager.setElAttr(action.el, "placeholder", action.getProp("text"));
		}
	},
	checked: 
	{
		perform: function(action) 
		{
			UIManager.setChecked(action.el, getBool(action.getProp("value")));
		}
	},	
	selected: 
	{
		perform: function(action) 
		{
			UIManager.setSelectedOption(action.el, action.getProp("value"));
		}
	},
	addClass: 
	{
		perform: function(action) 
		{
			UIManager.addClassToEl(action.el, action.getProp("class"));
		}
	},
	removeClass: 
	{
		perform: function(action) 
		{
			UIManager.removeClassFromEl(action.el, action.getProp("class"));
		}
	},
	switchClass: 
	{
		perform: function(action) 
		{
			UIManager.switchClassOnEl(action.el, action.getProp("class"));
		}
	},
	setValue: 
	{
		perform: function(action) 
		{
			UIManager.setValue(action.el, action.getProp("value", ""));
		}
	},
	resetValue: 
	{
		perform: function(action) 
		{
			UIManager.resetValue(action.el);
		}
	},
	select: 
	{
		perform: function(action) 
		{
			UIManager.select(action.el);
		}
	},
	restorableValue: 
	{
		perform: function(action) 
		{
			var storageKey = action.getProp("storageKey");
			
			UIManager.setValue(action.el, Remember.that(storageKey, UIManager.getValue(action.el)))
			
			EventsManager.subscribeOnce(Events.PageDirector.pageUnloaded, function() {
				Remember.please(storageKey, UIManager.getValueTrimmed(action.el));;
			});
		}
	},
	rememberGlob: 
	{
		perform: function(action) 
		{
			Remember.please(action.getProp("remember"), Templater.Glob.get(action.getProp("glob")));
		}
	},
	setSrc: 
	{
		perform: function(action) 
		{
			UIManager.setElAttr(action.el, "src", action.getProp("value"));
		}
	},
	setSrcAsync:
	{
		perform: function(action)
		{
      var img = new Image();

      img.onload = function ()
			{
				img.style.cssText = action.el.style.cssText;
				action.el.parentNode.replaceChild(img, action.el);
			};
      img.src = action.getProp("value");
		}
	},
	setHref: 
	{
		perform: function(action) 
		{
			UIManager.setElAttr(action.el, "href", action.getProp("href"));
		}
	},
	toggleDisplay: 
	{
		perform: function(action) 
		{
			UIManager.toggleElDisplay(action.el, action.getProp("display"));
		}
	},
	adaptHeight: 
	{
		perform: function(action) 
		{
			UIManager.adaptElHeight(action.el);
		}
	},
	autoHeight: 
	{
		perform: function(action) 
		{
			UIManager.autogrowTetarea(action.el);
		}
	},
	scrollTop: 
	{
		perform: function(action) 
		{
			UIManager.scrollTop(action.el);
		}
	},
	scrollIntoView: 
	{
		perform: function(action) 
		{
			UIManager.scrollIntoView(action.el);
		}
	},
	scrollDown: 
	{
		perform: function(action) 
		{
			UIManager.scrollDown(action.el);
		}
	},
  scrollRight:
  {
    perform: function(action)
    {
      UIManager.scrollRight(action.el);
    }
  },
	preserveScroll: 
	{
		perform: function(action) 
		{
			UIManager.setElAttr(action.el, "temp-js-preserveScroll-value", action.el.scrollHeight - action.el.scrollTop);
		}
	},
	restoreScroll: 
	{
		perform: function(action) 
		{
			var preservedScroll = UIManager.getElAttr(action.el, "temp-js-preserveScroll-value");
			
			if(preservedScroll !== undefined) 
			{
				UIManager.setScrollTop(action.el, action.el.scrollHeight - preservedScroll);
				UIManager.removeElAttr(action.el, "temp-js-preserveScroll-value")
			}
		}
	},
	noScrollingPropagation: 
	{
		perform: function(action) 
		{
			UIManager.preventScrollPropagation(action.el);
		}
	},
	time: 
	{
		perform: function(action) 
		{
			return UIManager.setHTML(action.el, 
				DateTimeManager.getFormattedDate(
					action.getProp("utc"), 
					action.getProp("prefix")));
		}
	},
	selectText:
	{
		perform: function(action)
		{
			UIManager.selectText(action.el);
		}
	},
	processForm: 
	{
		/**
		 * 
		 * @param {Templater.Action} action
		 * @returns {undefined}
		 */
		perform: function(action) 
		{
			var formData = {};
			var form = new FormData(action.el);
			
			performOnElsList(action.el.querySelectorAll("[name]"), function(el) 
			{
				var name = UIManager.getElAttr(el, "name");
				var value = UIManager.getValue(el);
				
				if(!formData[name+"_array"]) {
					formData[name+"_array"] = [];
				}
		
				if(UIManager.getElAttr(el, "type") === "checkbox") {
					if(el.checked) {
						formData[name] = "on";
						formData[name+"_array"].push(value); // 
					}
					else 
					{
						formData[name] = "off";
					}
				}
				else 
				{
					formData[name] = value;
					formData[name+"_array"].push(value); // 
				}
			});
			
			action.setData("formData", formData);
			action.setData("form", form);
			
			var isValid = true;
			
			performOnElsList(action.el.querySelectorAll("[data-js-form-validation]"), function(el) 
			{
				var func = UIManager.getElAttr(el, "data-js-form-validation");
				
				var result = new Temparser(null, action.getData(), null).evaluateExpression(func)(UIManager.getValueTrimmed(el));
				
				if(!result.valid) 
				{
					isValid = false;
					PageDirector.showOperationError(result.message);
					return false;
				}
			});
			
			var whenValid = action.getProp("ifValid");
			
			if(isValid && whenValid) {
				Templater.processActions(whenValid, action.el, action.getData(), action.parentEl);
			}
		}
	},
	clearForm:
	{
		perform: function(action)
		{
      performOnElsList(action.el.querySelectorAll("[name]"), function(el)
      {
      	UIManager.resetValue(el);
      });
		}
	},
  onFormChanges:
	{
		perform: function(action)
		{
      var onChangeAction = action.getProp("perform");

      if(onChangeAction)
      {
        performOnElsList(action.el.querySelectorAll("[name]"), function(el)
        {
          UIManager.addEvent(el, "change keydown", function()
          {
            Templater.processActions(onChangeAction, action.el, action.getData(), action.parentEl);
          });
        });
      }
		}
	},
	link: 
	{
		perform: function(action) 
		{
			NavigationManager.goToUrl(new NavigationManagerUrl().createFromRelative(action.getProp("url"), action.el.href));
		}
	},
	setUrl: 
	{
		perform: function(action) 
		{
			NavigationManager.replaceUrl(new NavigationManagerUrl().createFromRelative(action.getProp("url"), action.el.href));
		}
	},
	image: 
	{
		perform: function(action) 
		{
      var placeholder = document.createElement("img");
      placeholder.src = action.getProp("preview", action.getProp("src"));
      Register.mainModal.showEl(placeholder);

      placeholder.onload = function()
			{
        var img = new Image();
        img.src = action.getProp("src");

        img.onload = function ()
        {
          placeholder.parentNode.replaceChild(img, placeholder);
        };
			};
		}
	},
	externalLink: 
	{
		perform: function(action) 
		{
			NavigationManager.goToExternalUrl(action.getProp("url"));
		}
	},
	popover: 
	{
		perform: function(action) 
		{
			var args = {
				triggerAction: action.getData("trigger"),
				onShow: action.getProp("onShow", "void"),
				onHide: action.getProp("onHide", "void"),
				hideOnClickOutside: action.getProp("hideOnClickOutside", false),
        display: action.getProp("display", "block")
			};
			
			UIManager.toggleElDisplay(action.el, args.display, function()
			{
				Templater.processActions(args.onShow, action.el, action.getData(), action.parentEl);
			},
			function() 
			{
				Templater.processActions(args.onHide, action.el, action.getData(), action.parentEl);
			});


			EventsManager.subscribeMulti([Events.PageDirector.pageUnloaded, Events.UI.hidePopovers], function()
			{
				UIManager.hideEl(action.el);
				Templater.processActions(args.onHide, action.el, action.getData(), action.parentEl);
			}, {
				once: true
			});

      if(args.hideOnClickOutside !== false)
      {
        UIManager.addEvent(document, "click", function(e)
        {
          if(isElHasParent(e.target, action.el) == false
            && isElHasParent(e.target, args.triggerAction.el) == false
            && UIManager.isElDisplay(action.el, "none") == false)
          {
            UIManager.hideEl(action.el);
            Templater.processActions(args.onHide, action.el, action.getData(), action.parentEl);

            e.preventDefault();
            // e.stopPropagation();
            return false;
          }
        });
			}
		}
	},
	charactersCounter: 
	{
		perform: function(action) 
		{
			var args = {
				input: getEl(action.getProp("inputId")),
				maxLength: action.getProp("maxLength"),
				whenAbove: action.getProp("above"),
				whenBelow: action.getProp("below"),
				onInput: action.getProp("onInput")
			};

			var curentLength = UIManager.getValue(args.input).length;

			action.setData("charactersCounter", {
				curentLength: curentLength,
				left: args.maxLength - curentLength
			});

			if(curentLength > args.maxLength) 
			{
				Templater.processActions(args.whenAbove, action.el, action.getData(), action.parentEl);
			}
			else 
			{
				Templater.processActions(args.whenBelow, action.el, action.getData(), action.parentEl);
			}

			Templater.processActions(args.onInput, action.el, action.getData(), action.parentEl);
		}
	},

  horizontalScrollController:
  {
	  perform: function(action)
    {
      var args = {
        target: getEl(action.getProp("targetId")),
        onchange: action.getProp("onchange")
      };

      function getSelectedIndex(target, totalNumber) {
        return Math.round(UIManager.getScrollLeft(target) / (target.scrollWidth / totalNumber))
      }

      var switchers = UIManager.getChildren(action.el, "._item");
      var selectedIndex = 0;

      performOnElsList(switchers, function(switcher, index)
      {
        UIManager.addEvent(switcher, "click touch", function()
        {
          UIManager.setScrollLeft(args.target, (index * (args.target.scrollWidth / switchers.length)));
        });
      });

      UIManager.addEvent(args.target, "scroll", function()
      {
        performOnElsList(switchers, function(item) {
          UIManager.removeClassFromEl(item, "_selected");
        });

        var newSelectedIndex = getSelectedIndex(args.target, switchers.length);

        UIManager.addClassToEl(switchers[newSelectedIndex], "_selected");

        if(selectedIndex != newSelectedIndex)
        {
          var data = action.getData();
          data.selectedIndex = newSelectedIndex;

          if(args.onchange) {
            Templater.processActions(args.onchange, action.el, data, action.parentEl);
          }
        }

        selectedIndex = newSelectedIndex;

      });
    }
  },
	
	fadeOut: 
	{
		perform: function(action) 
		{
			UIEffects.fadeOut(action.el);
		}
	},

	destroy:
	{
    perform: function(action)
    {
      UIManager.removeEl(action.el);
    }
	},

	flash:
	{
		perform: function(action)
		{
			UIEffects.flash(action.el, action.getProp("class"), function() {
        if(action.getProp("onFlash")) {
          Templater.processActions(action.getProp("onFlash"), action.el, action.getData(), action.parentEl);
				}
			});
		}
	},
	
	filterTagMatch: 
	{
		perform: function(action) 
		{
			var args = {
				id: action.getProp("id"),
				tagInfo: action.getData("data").tag,
				filters: action.getProp("filters", "kindMatchesOrAll, searchEmptyOrMatch").splitTrim(","),
				true: action.getProp("true"),
				false: action.getProp("false")
			};
			
			var filter = Templater.Glob.get(args.id, null, {
				search: "",
				kind: ""
			});
			
			var filters = 
			{
				kindMatches: function(tagInfo, filter) 
				{
					return tagInfo.kind == filter.kind;
				},
				kindMatchesOrAll: function(tagInfo, filter) 
				{
					return (tagInfo.kind == filter.kind || filter.kind == "all");
				},
				kindMatchesOrAllOrEmpty: function(tagInfo, filter) 
				{
					return (tagInfo.kind == filter.kind || filter.kind == "all" || filter.kind == "");
				},
				searchNotEmpty: function(tagInfo, filter) 
				{
					return filter.search != "";
				},
				searchMatch: function(tagInfo, filter) 
				{
					return tagInfo.tag.indexOf(UIFormat.prepareTag(filter.search)) !== -1;
				},
				searchEmpty: function(tagInfo, filter) 
				{
					return filter.search == "";
				},
				searchEmptyOrMatch: function(tagInfo, filter) 
				{
					return (filter.search == "" || tagInfo.tag.indexOf(UIFormat.prepareTag(filter.search)) !== -1);
				}
			};
			
			
			var afterAction = args.true;
			performOnElsList(args.filters, function(func) 
			{
				if(!filters[func](args.tagInfo, filter)) 
				{
					afterAction = args.false;
					return false;
				}
			});

			Templater.processActions(afterAction, action.el, action.getData(), action.parentEl);

//			if((args.tagInfo.kind == filter.kind || filter.kind == "all" || filter.kind == undefined) 
//			&& (filter.search == "" || args.tagInfo.tag.indexOf(UIFormat.prepareTag(filter.search)) !== -1)) 
//			{
//				Templater.processActions(args.true, action.el, action.getData(), action.parentEl);
//			}
//			else 
//			{
//				Templater.processActions(args.false, action.el, action.getData(), action.parentEl);
//			}
		}
	},
	
	filterUserChat: 
	{
		perform: function(action) 
		{
			var args = {
				chatInfo: action.getData("data").chat,
				true: action.getProp("true"),
				false: action.getProp("false")
			};
			
			var filter = Templater.Glob.get('UserChatsFilter', null, 
				Remember.that(Remember.my.userChatsFilter, {
					chatName: "",
					onlineOnly: false,
					hideInvites: false,
					hideFinished: false
				}));
			
			var afterAction = args.true;

			if(filter.onlineOnly == true && args.chatInfo.strangerInfo.isActive == false
			|| filter.hideInvites == true && args.chatInfo.wasAccepted == false
			|| filter.hideFinished == true && args.chatInfo.isClosed == true
			|| filter.chatName && args.chatInfo.nameCustom.toLowerCase().indexOf(OR(filter.chatName, "").toLowerCase()) === -1)
			{
				afterAction = args.false;
			}

			Templater.processActions(afterAction, action.el, action.getData(), action.parentEl);
		}
	},
	
	setStyle: 
	{
		perform: function(action) 
		{
			UIManager.setStyle(action.el, action.getProp("style"), action.getProp("value"));
		}
	},
	
	json2html: 
	{
		perform: function(action) 
		{
			UIManager.json2html(action.el);
		}
	}
};

Events.UI = 
{
	hidePopovers: "hidePopovers"
};

Events.registerEvents(Events.UI);

Templater.MacrosManager = 
{
	attrs: {
		macros: "data-js-macros"
	},
	available: {},
	
	retrive: function(el) 
	{
		performOnElsList(el.querySelectorAll("["+Templater.MacrosManager.attrs.macros+"]"), function(el) 
		{
			if(!UIManager.getElAttr(el, Templater.attrs.digested) ) 
			{
				var name = UIManager.getElAttr(el, Templater.MacrosManager.attrs.macros);
				Templater.MacrosManager.available[name] = new Templater.Macros(name, el);

				UIManager.setElAttr(el, Templater.attrs.digested, true);
			}
		});
	},
};

Templater.Macros = function(name, el) 
{
	this.name = name;
	this.attr = "data-js-apply-"+name;
	this.el = el;
	this.actions = Templater.ActionsParser.retriveNameWithAlliases(UIManager.getElAttr(el, Templater.attrs.action));;
};

var Temparser = function(content, data, name) 
{
	var _this = this;
	this.content = content;
	this.name = name;
	this.data = data;
	
	this.format = function() 
	{
		return this.content.replace(new RegExp("{{(.[^{{]+)}}", "g"), function(match, matchInner) 
		{
			var value = _this.evaluate(matchInner, match);

			return OR(value, "");
		}).trim();
	};
	
	this.evaluate = function(expression, _default) 
	{
		var value = null;
		
		var conditionParts = expression.splitTrim("?");
		
		// if condition
		if(conditionParts.length > 1) 
		{
			var condition = conditionParts[0];
			
			var whenTrue = conditionParts[1];
			var whenFalse = null;
			
			// if "else" is presented
			var outcomes = conditionParts[1].splitTrim(":");
			
			if(outcomes.length > 1) 
			{
				whenTrue = outcomes[0];
				whenFalse = outcomes[1];
			}
			
			if(_this.evaluateCondition(condition)) 
			{	
				if(whenTrue) {
					value = _this.evaluate(whenTrue);
				}	
			}
			else 
			{
				if(whenFalse) {
					value = _this.evaluate(whenFalse);
				}	
			}
			
		}
		else 
		{
			// no condition, jsut an expression
			value = _this.evaluateCondition(conditionParts[0], _default);
		}
		
		return value;
	};
	
	this.evaluateCondition = function(expression) 
	{
		var result = false;
		
		var ands = expression.splitTrim(/\sand\s/);
		var ors = expression.splitTrim(/\sor\s/);
		
		if(ands.length > 1) 
		{
			for(var i = 0; i < ands.length; i++) 
			{
				if(_this.evaluateCondition(ands[i])) 
				{
					result = true;
				} else {
					result = false;
					break;
				}
			}
		} 
		else if(ors.length > 1) 
		{	
			var ors = expression.splitTrim(" or ");
			
			for(var i = 0; i < ors.length; i++) 
			{
				if(_this.evaluateCondition(ors[i])) 
				{
					result = true;
					break;
				}
			}
		}
		else 
		{
			if(expression.match(/^[\"'].*[\"']$/g) === null) 
			{
				// checking if it's entry like 'a == b'
				// not the best approach
				var availableCompars = []; 
				performOnEveryKey(Temparser.comparisons._map, function(key, val) {
					availableCompars.push(val);
				});
				
				var matches = expression.match("(.+)("+availableCompars.join("|")+")(.+)");
					
				if(matches) 
				{
					matches = matches.trim();
					var val1 = _this.evaluateExpression(matches[1]);
					var val2 = _this.evaluateExpression(matches[3]);

					var compareFunc = Temparser.comparisons._find(matches[2]);

					if(compareFunc) 
					{
						result = Temparser.comparisons[compareFunc](val1, val2);
					}
					else 
					{
						Debug.error("Tempater.comparison: ["+matches[1]+"] not found.");
					}
				}
				else 
				{
					result = _this.evaluateExpression(expression, false);
				}
				
			} else {
				result = expression.replace(/^[\"']|[\"']$/g, '');
			}
		}
		
		return result;
		
	};
	
	this.evaluateExpression = function(expression, _default) 
	{
		var value = _default;
		
		var executive = expression.match(/(.[^(]+)\(?/);
		var arguments = expression.match(/[(](.*)[)]/);

		if(executive && executive.length > 0) 
		{
			if(arguments && arguments.length > 0) 
			{
				var func = _this.parseDataExpression(executive[1], undefined);
				var funcArguments = _this.parseExpressionArguments(arguments[1]);

				if(func.value !== undefined) 
				{
					value = func.value.apply(func.obj, funcArguments);
				}
			}
			else 
			{
				value = _this.parseValue(executive[1], _default);
			}
		}
		else 
		{
			value = _this.parseValue(expression, _default);
		}
		
		return value;
	};
	
	this.parseDataExpression = function(expression, _default) 
	{
		var result = {
			obj: null,
			value: _default
		};
		
		if(expression) 
		{
			var tempData = _this.data;
			var keysPath = expression.splitTrim(".");
		
			performOnElsList(keysPath, function(key, i) 
			{
				if(tempData !== null 
				&& tempData !== undefined 
				&& tempData[key] !== undefined) 
				{
					result.obj = tempData;
					result.value = tempData = tempData[key];
				}
				else if (i == 0 && window[key] !== undefined) 
				{
					result.obj = window;
					result.value = tempData = window[key];
				}
				else 
				{
					Debug.error("Temparser: ["+_this.name+"] Cannot find data for [" + expression + "]");

					Debug.error("== Defails ==");
					Debug.error("Data Available: ");
					Debug.error(_this.data);
					Debug.error("Content: ");
					Debug.error(_this.content);
					Debug.error("== End ==");
					result.value = _default;
				}
			});
		}
		
		return result;
	};
	
	this.parseExpressionArguments = function(expression) 
	{
		var retArgs = [];
		
		if(expression) 
		{
			var args = expression.split(",");
			
			performOnElsList(args, function(item) 
			{
				retArgs.push(_this.parseValue(item));
			});
		}
		
		return retArgs;
	};
	
	this.parseValue = function(expression, _default) 
	{
		var value = _default;
		
		if(expression) 
		{
			expression = expression.trim().replace(/&quot;/g, '\"');
			if(expression.match(/^[\"']|[\"']$/g) !== null) 
			{
				value = expression.replace(/^[\"']|[\"']$/g, '');
			}
			else if(isNumeric(expression)) 
			{
				value = expression;
			}
			else 
			{
				switch (expression) 
				{
					case "true":
						value = true;
						break;

					case "false":
						value = false;
						break;

					case "this":
						value = _this.data;
						break;

					case "null":
						value = null;
						break;

					default:
						value = _this.parseDataExpression(expression, _default).value;
				}
			}
		}
		
		return value;
	}
	
	return this;
};

Temparser.comparisons = 
{
	_find: function(cmp) 
	{
		for(var itm in this._map) {
			if(cmp == this._map[itm]) {
				return itm;
			}
		}
	},
	_map: {
		notEqq: "!==",
		notEq: "!=",
		eqq: "===",
		eq: "==",
		moreOrEq: ">=",
		more: ">",
		lessOrEq: "<=",
		less: "<",
		minus: "minus",
		plus: "plus"
	},
	eq: function(a, b) 
	{
		return a == b;
	},
	eqq: function(a, b) 
	{
		return a === b;
	},
	notEq: function(a, b) 
	{
		return a != b;
	},
	notEqq: function(a, b) 
	{
		return a !== b;
	},
	more: function(a, b) 
	{
		return (parseFloat(a) > parseFloat(b));
	},
	moreOrEq: function(a, b) 
	{
		return (parseFloat(a) >= parseFloat(b));
	},
	less: function(a, b) 
	{
		return (parseFloat(a) < parseFloat(b));;
	},
	lessOrEq: function(a, b) 
	{
		return (parseFloat(a) <= parseFloat(b));;
	},
	minus: function(a, b)
	{
		return parseFloat(a) - parseFloat(b);
	},
  plus: function(a, b)
  {
    return parseFloat(a) + parseFloat(b);
  }
};

var TemplatesManager = new function() 
{
	var _this = this;
	this.loadedTemplates = {};
	this.templateRelations = {};
	
	this.attrs = {
		declaration: "data-js-template"
	};
	
	function formatAttrSelector(attr) 
	{
		return "["+attr+"]";
	}

	this.retrieveTemplates = function(element, parent) 
	{
		performOnElsList(element.querySelectorAll(formatAttrSelector(_this.attrs.declaration)), 
		function(el) {
			_this.retrieveTemplate(el, parent);
		});
	};
	
	this.retrieveTemplate = function(el, parent) 
	{
		var templateName = UIManager.getElAttr(el, _this.attrs.declaration);
		
		if(_this.loadedTemplates[templateName] === undefined) 
		{
			// first retrieve included templates to prevent them from rendering as a part of this template
			_this.retrieveTemplates(el, parent);
			
			_this.loadedTemplates[templateName] = UIManager.getHTML(el);
			if(parent) {
				_this.templateRelations[parent] = templateName;
			}

			UIManager.removeEl(el);
		}
	};

	this.getHTML = function(templateName) 
	{
		if(this.loadedTemplates[templateName] !== undefined) 
		{
			return this.loadedTemplates[templateName];
		}
		else 
		{
			throw "Cannot find template: '"+templateName+"'";
		}
	};

	this.format = function(templateName, data) 
	{
		return new Temparser(this.getHTML(templateName), data, templateName).format();
	};
};

var Threader = 
{
	putInQueue: function(_func, callback) 
	{
		Timeout.set(function() 
		{
			var res = _func();

			if(callback) {
				callback(res);
			}
			
		}, 1);
	},
	
	run: function(_func, options) 
	{
		if(!options) {
			options = {
				callback: null,
				inQ: false
			};
		}
		
		if(options.inQ) 
		{
			Timeout.set(function() 
			{
				var res = _func();

				if(options.callback) {
					options.callback(res);
				}

			}, 1);
		}
		else 
		{
			var res = _func();

			if(options.callback) {
				options.callback(res);
			}
		}
	}
};

var Timeout = new function() 
{
	this.reset = function(timeout) 
	{
		if(timeout != null) {
			clearTimeout(timeout);
			timeout = null;
		}
	};
	
	this.set = function(func, time) 
	{
		var timeout = setTimeout(function() {
			func();
			timeout = null;
		}, time);
		
		return timeout;
	};

	this.debounce = function(func, time, prevTimeout)
	{
		if(prevTimeout) {
			Timeout.reset(prevTimeout);
			Debug.info("Debounced");
		}

    var timeout = setTimeout(function()
		{
      func();
      timeout = null;
    }, time);

    return timeout;
	}
};

var UIManager = new function() 
{
	this.appendCSS = function(el, css) 
	{
		var cssEl = document.createElement("style");
		cssEl.type = "text/css";
		cssEl.innerHTML = css;
		el.appendChild(cssEl);
	};
	
	this.appendJS = function(el, js) 
	{
		var jsEl = document.createElement("script");
		jsEl.type = "text/javascript";
		jsEl.innerHTML = js;
		el.appendChild(jsEl);
	};
	
	this.appendJSsrc = function(el, src) 
	{
		var jsEl = document.createElement("script");
		jsEl.type = "text/javascript";
		jsEl.src = src;
		el.appendChild(jsEl);
	};
	
	this.getChild = function(el, selector) 
	{
		return el.querySelector(selector);
	};
	
	this.getChildren = function(el, selector) 
	{
		return el.querySelectorAll(selector);
	};
	
	this.elHassAttr = function(el, attribute) 
	{
		return el.getAttribute(attribute) !== null;
	};
	
	this.getElAttr = function(el, attribute, _default) 
	{
		return UIManager.elHassAttr(el, attribute) ? el.getAttribute(attribute) : _default;
	};
	
	this.setElAttr = function(el, attribute, value) 
	{
		return el.setAttribute(attribute, value);
	};
	
	this.removeElAttr = function(el, attribute) 
	{
		return el.removeAttribute(attribute);
	};
	
	this.elHasData = function(el, dataAttribute) 
	{
		return UIManager.elHassAttr(el, "data-"+dataAttribute);
	};
	
	this.getElData = function(el, dataAttribute, _default) 
	{
		return UIManager.getElAttr(el, "data-"+dataAttribute, _default);
	};
	
	this.setElData = function(el, dataAttribute, value) 
	{
		return UIManager.setElAttr(el, "data-"+dataAttribute, value);
	};
	
	this.isElDisplay = function(el, display) 
	{
		return el.style.display == display;
	};
	
	this.hideEl = function(el) 
	{
		el.style.display = 'none';
		
		EventsManager.fire(Events.UIManager.elHidden, el);
		
		return UIManager;
	};
	
	this.hideElsList = function(elClass) 
	{
		performOnElsList(getAllElsByClass(elClass), function(el) {
			UIManager.hideEl(el);
		});
		
		return UIManager;
	};

	this.showEl = function(el, display) 
	{
		el.style.display = display ? display : 'block';
		
		EventsManager.fire(Events.UIManager.elDisplayed, el);
		
		return UIManager;
	};
	
	this.conditionalDisplay = function(el, condtion, display) 
	{
		if(condtion) {
			UIManager.showEl(el, display);
		} else {
			UIManager.hideEl(el);
		}
	};
	
	this.showElsList = function(elClass, display) 
	{
		performOnElsList(getAllElsByClass(elClass), function(el) {
			UIManager.showEl(el, display);
		});
	};
	
	this.toggleElDisplay = function(el, display, showCallback, hideCallback) 
	{
		if(display === undefined) {
			display = 'block';
		}
		
		if(!this.isElDisplay(el, display)) 
		{
			this.showEl(el, display);
			if(showCallback) showCallback();
		}
		else
		{
			this.hideEl(el);
			if(hideCallback) hideCallback();
		}
		
		return el.style.display;
	};
	
	this.toggleElsListDisplay = function(elClass, display) 
	{
		performOnElsList(getAllElsByClass(elClass), function(el) {
			UIManager.toggleElDisplay(el, display);
		});
		
		return UIManager;
	};
	
	this.enEl = function(el) 
	{
		el.disabled = false;
	};
	
	this.disEl = function(el) 
	{
		el.disabled = true;
	};
	
	this.setFocus = function(el) 
	{
		el.focus();
	};
	
	this.getClass = function(el) 
	{
		return el.className;
	};
	
	this.setClass = function(el, className) 
	{
		el.className = className;
	};
	
	this.getHTML = function(el) 
	{
		return el.innerHTML;
	};
	
	
	this.setHTML = function(el, html) 
	{
		el.innerHTML = html;
		
		EventsManager.fire(Events.UIManager.htmlChanged, el);
		EventsManager.fire(Events.UIManager.elementContentChanged, el);
	};
	
	this.addHTML = function(el, html, atTop) 
	{
		if(atTop === true) 
		{
			el.innerHTML = html + el.innerHTML;
		}
		else 
		{
			el.innerHTML += html;
		}
		
		EventsManager.fire(Events.UIManager.htmlChanged, el);
		EventsManager.fire(Events.UIManager.elementContentChanged, el);
	};
	
	this.addNodeFromHTML = function(el, html, atTop, beforeEl) 
	{
		var tmpNode = document.createElement('span');
		UIManager.setHTML(tmpNode, html); // trimming so that first child will not be text
		el.appendChild(tmpNode);
		
		if(atTop || beforeEl) 
		{
			if(atTop) {
				beforeEl = el.firstChild;
			}
		}
		
		while (tmpNode.childNodes.length > 0) {
			el.insertBefore(tmpNode.childNodes[0], beforeEl);
		}
		
		UIManager.removeEl(tmpNode);
		
		EventsManager.fire(Events.UIManager.htmlChanged, el);
		EventsManager.fire(Events.UIManager.elementContentChanged, el);
		
		return el;
	};
	
	this.setNodeFromHTML = function(el, html) 
	{
		while (el.firstChild) {
			el.removeChild(el.firstChild);
		}
		
		UIManager.addNodeFromHTML(el, html);
		
		EventsManager.fire(Events.UIManager.htmlChanged, el);
		EventsManager.fire(Events.UIManager.elementContentChanged, el);
	};
	
	this.replaceElWithHTML = function(el, html) 
	{
		var newNode = UIManager.addNodeFromHTML(el.parentNode, html, false, el);
		UIManager.removeEl(el);
		
		EventsManager.fire(Events.UIManager.htmlChanged, el);
		EventsManager.fire(Events.UIManager.elementContentChanged, el);
		
		return newNode;
	};
	
	this.addChild = function(el, child) 
	{
		el.appendChild(child);
	};
	
	this.setChild = function(el, child) 
	{
		UIManager.clearEl(el);
		UIManager.addChild(el, child);
	};
	
	this.removeEl = function(el) 
	{
		if(el.parentNode) {
			el.parentNode.removeChild(el);
		}
	};
	
	this.clearEl = function(el) 
	{
		UIManager.setNodeFromHTML(el, "");
	};
	
	this.json2html = function(el) 
	{
		UIManager.setHTML(el, JSON.parse(UIManager.getHTML(el)));
	};
	
	this.setText = function(el, text) 
	{
		el.textContent = text;
		
		EventsManager.fire(Events.UIManager.elementContentChanged, el);
	};
	
	this.getValue = function(el) 
	{
		return el.value;
	};
	
	this.getValueTrimmed = function(el) 
	{
		return UIManager.getValue(el).trim();
	};
	
	this.setValue = function(el, value) 
	{
		el.value = value;
		
		EventsManager.fire(Events.UIManager.elementContentChanged, el);
	};
	
	this.appendValue = function(el, value) 
	{
		el.value += value;
		
		EventsManager.fire(Events.UIManager.elementContentChanged, el);
	};
	
	this.resetValue = function(el) 
	{
		el.value = "";
		
		EventsManager.fire(Events.UIManager.elementContentChanged, el);
	};
	
	this.select = function(el) 
	{
		el.select();
	};

	this.selectText = function(el)
	{
    if (document.selection)
    { // IE
      var range = document.body.createTextRange();
      range.moveToElementText(el);
      range.select();
    }
    else if (window.getSelection)
    {
      window.getSelection().selectAllChildren(el);
      // var range = document.createRange();
      // range.selectNode(el);
      // window.getSelection().removeAllRanges();
      // window.getSelection().addRange(range);
    }
	};
	
	this.isChecked = function(el) 
	{
		return el.checked;
	};
	
	this.setChecked = function(el, value) 
	{
		if(value === undefined) {
			value = true;
		}
		
		el.checked = getBool(value);
	};
	
	this.setSelectedOption = function(el, key) 
	{
		el.options[UIManager.getSelectorIndexOfOption(el, key)].selected = true;
	};
	
	this.getSelectedOption = function(el) 
	{
		return UIManager.getSelectorOptionOfIndex(el, el.options.selectedIndex);
	};
	
	this.getSelectorIndexOfOption = function(selector, key) 
	{
		var index = 0;
		performOnElsList(selector.options, function(option, i) {
			if(UIManager.getValue(option) == key) 
			{
				index = i;
			}
		});
		
		return index;
	};
	
	this.getSelectorOptionOfIndex = function(selector, index) 
	{
		return UIManager.getValueTrimmed(selector.options[index]);
	};
	
	this.scrollIntoView = function(el, countfixedNavbar) 
	{
		el.scrollIntoView();
		
		if(countfixedNavbar && AppInfo.isDesctop()) 
		{
			var scrolledY = window.scrollY;
			
			if(scrolledY) 
			{
				window.scroll(0, scrolledY - Register.layout.getHeaderHeight(true));
			}
		}
	};
	
	this.getScrollTop = function(el) 
	{
		return el.scrollTop;
	};
	
	this.setScrollTop = function(el, val) 
	{
		el.scrollTop = val;
	};
	
	this.scrollTop = function(el) 
	{
		UIManager.setScrollTop(el, 0);
	};
	
	this.scrollDown = function(el) 
	{
		UIManager.setScrollTop(el, el.scrollHeight);
	};

	this.getScrollLeft = function(el)
  {
	  return el.scrollLeft;
  };

  this.setScrollLeft = function(el, val)
  {
    el.scrollLeft = val;
  };

  this.scrollRight = function(el)
  {
    UIManager.setScrollLeft(el, el.scrollWidth);
  };
	
	
	this.getScrollDif = function(el) 
	{
		return el.scrollHeight - el.scrollTop;
	};
	
	this.setScrollDif = function(el, scrollDif) 
	{
		el.scrollTop = el.scrollHeight - scrollDif;
	};
	
	this.addClassToEl = function(el, className)
	{
		if(el.className) 
		{
			this.removeClassFromEl(el, className);
			
			var elClassList = el.className.split(" ");
			elClassList.push(className);
			el.className = elClassList.join(" ");
		} else {
			el.className = className;
		}
	};

	this.removeClassFromEl = function(el, classNames)
	{
		if(el.className) 
		{
			var classesToRemove = classNames.split(" ");
			
			var elClassList = el.className.split(" ");
			var newClassList = new Array();
			elClassList.forEach(function(className) 
			{
				if(classesToRemove.indexOf(className) === -1) 
				{
					newClassList.push(className);
				}
			});
			
			el.className = newClassList.join(" ");
		}
	};
	
	this.switchClassOnEl = function(el, classNames)
	{
		var classesToSwitch = classNames.split(" ");

		performOnElsList(classesToSwitch, function(className) 
		{
			if(UIManager.hasClass(el, className)) {
				UIManager.removeClassFromEl(el, className);
			} else {
				UIManager.addClassToEl(el, className);
			}
		});
	};
	
	this.hasClass = function(el, className) 
	{
		return (el.className && el.className.indexOf(className) > -1);
	};
	
	this.toggleElClass = function(el, className) 
	{
		if(this.hasClass(el, className)) 
		{
			this.removeClassFromEl(el, className);
		}
		else 
		{
			this.addClassToEl(el, className);
		}
	};
	
	this.getStyle = function(el, style, isInt) 
	{
		var res = el.style[style];
		if(isInt) 
		{
			res = parseInt(res);
		}
		
		return res;
	};
	
	this.setStyle = function(el, style, value, units) 
	{
		if(units) 
		{
			value += units;
		}
		
		el.style[style] = value;
	};

	this.applyClassToCollectionOnCondition = function(collection, className, condition)
	{
		for (var i = 0; i < collection.length; i++) {
			if (condition) {
				this.addClassToEl(collection[i], className);
			} else {
				this.removeClassFromEl(collection[i], className);
			}
		}
	};
	
	this.addEvent = function(els, events, handler) 
	{
		performOnElsList(events.split(" "), function(event) {
			if(0 > ["[object Array]", "[object NodeList]", "[object HTMLCollection]"].indexOf(Object.prototype.toString.call(els))) {
				els = [els];
			}
			
			performOnElsList(els, function(el) 
			{
				var h = function(e) 
				{
					if(handler(e, el) === false) {
						el.removeEventListener(event, h);
					}
				};
				el.addEventListener(event, h);
			});
		});
	};
	
	this.addEventNoDefault = function(els, events, handler) 
	{		
		UIManager.addEvent(els, events, function(e) {
			e.preventDefault();
			handler(e, els);
		});
	};
	
	this.removeChildren = function(el) 
	{
		while (el.firstChild) {
			 el.removeChild(el.firstChild);
		}
	};
	
	this.getElComputedStyle = function(el, style, isInt)
	{
		var res = window.getComputedStyle(el).getPropertyValue(style);
		if(isInt) 
		{
			return parseInt(res);
		}
		
		return res;
	};
	
	
	this.preventScrollPropagation = function(el)
	{
		var onScroll = function(ev) {
			
			var el = this,
				 scrollTop = this.scrollTop,
				 scrollHeight = this.scrollHeight,
				 height = UIManager.getElComputedStyle(el, "height", true),
				 delta = ev.wheelDelta,
				 up = delta > 0;

			var prevent = function() {
				 ev.stopPropagation();
				 ev.preventDefault();
				 ev.returnValue = false;
				 return false;
			}

			if (!up && -delta > scrollHeight - height - scrollTop) {
				 // Scrolling down, but this will take us past the bottom.
				 el.scrollTop = scrollHeight;
				 return prevent();
			} else if (up && delta > scrollTop) {
				 // Scrolling up, but this will take us past the top.
				 el.scrollTop = 0;
				 return prevent();
			}
		};
		
		el.addEventListener('mousewheel', onScroll);
		el.addEventListener('DOMMouseScroll', onScroll);
	};
	
	
	this.adaptElHeight = function(el) 
	{
		if(!isFinite(UIManager.getStyle(el, "height", true))) 
		{
			UIManager.setStyle(el, "height", 0, "px");
		}
		
		for(var i = UIManager.getStyle(el, "height", true); i >= 0 ; --i) 
		{
			UIManager.setStyle(el, "height", i, "px");
			
			if (el.scrollHeight > el.clientHeight) 
			{
				UIManager.setStyle(el, "height", el.scrollHeight + 4, "px");
				break;
			}
		}
	};
	
	this.autogrowTetarea = function(el) 
	{
		var prevLengthDataAttr = "prev-length";
		
		UIManager.setElData(el, prevLengthDataAttr, UIManager.getValue(el).length);
		
		UIManager.addEvent(el, "input change", function(e) 
		{
			if(UIManager.getValue(el).length != UIManager.getElAttr(el, prevLengthDataAttr)) 
			{
				UIManager.adaptElHeight(el);

				UIManager.setElData(el, prevLengthDataAttr, UIManager.getValue(el).length);
			}
		});
		
		UIManager.adaptElHeight(el);
	};
	
	this.rotate = function(el, degrees)
	{
		var rotateVal = "rotate("+degrees+"deg)";
		
		switch(degrees % 360) {
			case 90:
				rotateVal += " translateY(-100%)";
				break;
				
			case 180:
				rotateVal += " translate(-100%, -100%)";
				break;
			
			case 270:
				rotateVal += " translateX(-100%)";
				break;
		}
		
		if(navigator.userAgent.match("Chrome")){
			el.style.WebkitTransform = rotateVal;
		} else if(navigator.userAgent.match("Firefox")){
			el.style.MozTransform = rotateVal;
		} else if(navigator.userAgent.match("MSIE")){
			el.style.msTransform = rotateVal;
		} else if(navigator.userAgent.match("Opera")){
			el.style.OTransform = rotateVal;
		} else {
			el.style.transform = rotateVal;
		}
	};
	
	this.drawRotated = function(ctx, canvas, image, degrees)
	{
		var width = UIManager.getElComputedStyle(canvas, "width", true);
		var height = width * (image.height / image.width);
		
		var cw = width, ch = height, cx = 0, cy = 0;

		//   Calculate new canvas size and x/y coorditates for image
		switch(degrees){
			  case 90:
					 cw = height;
					 ch = width;
					 cy = height * (-1);
					 break;
			  case 180:
					 cx = width * (-1);
					 cy = height * (-1);
					 break;
			  case 270:
					 cw = height;
					 ch = width;
					 cx = width * (-1);
					 break;
		}

		//  Rotate image
		canvas.width = cw;
		canvas.height = ch;
		
		var hRatio = canvas.width  / image.width    ;
		var vRatio =  canvas.height / image.height  ;
		var ratio  = (image.height > image.width) ? Math.min(hRatio, vRatio) : Math.max(hRatio, vRatio);
		var centerShift_x = ( canvas.width - image.width*ratio ) / 2;
		var centerShift_y = ( canvas.height - image.height*ratio ) / 2;  
		
		ctx.rotate(degrees * Math.PI / 180);
		ctx.drawImage(image, 0, 0, 
								image.width, 
								image.height, 
								centerShift_x + cx, 
								centerShift_y + cy, 
								image.width*ratio, 
								image.height*ratio);
		
		//ctx.restore();
	};
};


//var UIDisplayToggler = new function() 
//{
//	this.restore = function(elId, showCallback, hideCallback, beforeCallback, defaultValue) 
//	{
//		SessionState.restoreDisplayState(getEl(elId), defaultValue);
//		if(UIManager.isElDisplay(getEl(elId), "block")) 
//		{
//			this.show(elId, showCallback, beforeCallback);
//		}
//		else 
//		{
//			this.hide(elId, hideCallback, beforeCallback);
//		}
//	};
//	
//	this.toggleEl = function(elId, showCallback, hideCallback, beforeCallback, display) 
//	{
//		if(!display) display = "block";
//		
//		if(UIManager.isElDisplay(getEl(elId), display)) 
//		{
//			this.hide(elId, hideCallback, beforeCallback, display);
//		}
//		else 
//		{
//			this.show(elId, showCallback, beforeCallback, display);
//		}
//		
//		return SessionState.saveDisplayState(getEl(elId));
//	};
//	
//	this.show = function(elId, callback, beforeCallback, display) 
//	{
//		call(beforeCallback);
//		
//		if(!display) display = "block";
//		
//		UIManager.showEl(getEl(elId), display);
//		SessionState.saveDisplayState(getEl(elId));
//		
//		call(callback);
//	};
//	
//	this.hide = function(elId, callback, beforeCallback) 
//	{
//		call(beforeCallback);
//		
//		UIManager.hideEl(getEl(elId));
//		SessionState.saveDisplayState(getEl(elId));
//		
//		call(callback);
//	};
//	
//	function call(callback) 
//	{
//		if(callback) {
//			callback();
//		}
//	}
//};

Events.UIManager = 
{
	htmlChanged: "htmlChanged",
	elementContentChanged: "elementContentChanged",
	elDisplayed: "elDisplayed",
	elHidden: "elHidden"
};

Events.registerEvents(Events.UIManager);

EventsManager.subscribe(Events.UIManager.elementContentChanged, function(el) {
	if(el.getAttribute && UIManager.getElData(el, "prev-length")) {
		UIManager.adaptElHeight(el);
	}
});

var UIFormat = new function() 
{
	this.isEmptyString = function(str) 
	{
		return str == null || str.trim().length == 0;
	};
	
	this.isEmptyTagsStr = function(str) 
	{
		return str.trim().replace("-", "").length == 0;
	};
	
	this.prepareTag = function(tag) 
	{
		return tag.toLowerCase().
				  replace(/[[\]{}()*+?.,\\^$|#]/g, "").
				  replace(/[\s_]/g, "-").
				  trim();
	};
	
	this.directInput = function(str) 
	{
		return UIFormat.nl2br(UIFormat.escapeHtml(str));
	};
	
	this.nl2br = function(str) 
	{
		return str.replace(/\n/g, '<br/>');
	};
	
	this.escapeHtml = function(str)
	{
		var map = {
			'&': '&amp;',
			'<': '&lt;',
			'>': '&gt;',
			'"': '&quot;',
			"'": '&#039;'
		};

		return str.replace(/[&<>"']/g, function (m) {
			return map[m];
		});
	};
};

var UIEffects = new function() 
{
	this.fadeOut = function(el) 
	{
		UIManager.addClassToEl(el, "opacity03");
	};

	this.flash = function(el, className, afterCallback)
	{
		UIManager.addClassToEl(el, className);

		Timeout.set(function()
		{
			UIManager.removeClassFromEl(el, className);

			if(afterCallback) {
				afterCallback();
			}
		}, 500);
	}
};

var WindowManager = 
{
	
};

var UITextManager = 
{
	attrs: {
		handler: "data-js-ui-texts-handler",
		text: "data-js-ui-text"
	},
	init: function() 
	{
		performOnElsList(document.querySelectorAll("["+UITextManager.attrs.handler+"]"), function(handler) 
		{
			var namespace = UIManager.getElAttr(handler, UITextManager.attrs.handler);
			
			performOnElsList(handler.querySelectorAll("["+UITextManager.attrs.text+"]"), function(text) 
			{
				var key = UIManager.getElAttr(text, UITextManager.attrs.text);
				var value = UIManager.getHTML(text);
				
				UITexts[namespace][key] = value;
			});
		});
	},

	format: function(text, data) 
	{
		if(data) 
		{
			for (var key in data) 
			{
				text = text.replaceAll("%"+key+"%", data[key]);
			};
		}

		return text;
	}
};

var UITexts = 
{
	registerTexts: function(list) 
	{
		var namespace = null;
		performOnEveryKey(UITexts, function(key, val) {
			if(val === list) {
				namespace = key;
			}
		});
		
		if(namespace) {
			performOnEveryKey(list, function(key, val) 
			{
				list[key] = [namespace, key].join(".");
			});
		} else {
			throw "Cannot register UITexts - no namespace found.";
		}
	}
};

UITexts.Generic = 
{
	projectName: "projectName",
	loading: "loading",
	
	saved: "saved",
	operationFailed: "operationFailed",
	
	alert_confirm: "confirm",
	alert_info: "alert_info",
	alert_error: "alert_error"
};

function uiText(text, data)  
{
	return UITextManager.format(text, data);
}

var AppInfo = 
{
	setup: 
	{
		version: null,
		uiVersion: null,
		
		domain: null,
		baseUrl: null,
		wssUrl: null,
		platform: null,
		OS: null,
		screen: null,
		
		messagesOrder: null,

		iconMain: null,
		iconAlt: null,

		sessionStorage: null,
		dataStorage: null
	},
	
	values: {
		uploadsManager: {
			allowedTypes: ["image/jpeg", "image/jpg", "image/gif", "image/png"],
			maxSizeBytes: 5000000
		},
		
		chat: {
			history: {
				messagesPerPage: 50
			}
		},
		
		updatesChecker: {
			ws: false
		}
	},
	
	getConnector: function() 
	{
		if(window.WebSocket)
		{
			return ConnectorTypes.ws;
		} else {
			return ConnectorTypes.http;
		}
	},
	
	getVersion: function() 
	{
		return this.setup.version;
	},
	
	getUIVersion: function() 
	{
		return this.setup.uiVersion;
	},
	
	getDomain: function() 
	{
		return this.setup.domain;
	},
	
	getBaseUrl: function() 
	{
		return this.setup.baseUrl;
	},
	
	getWssUrl: function() 
	{
		return this.setup.wssUrl;
	},
	
	getPlatform: function() 
	{
		return this.setup.platform;
	},
	
	getOS: function() 
	{
		return this.setup.OS;
	},
	
	getScreen: function() 
	{
		return this.setup.screen;
	},

	isMobile: function()
	{
		return this.setup.screen == ScreenTypes.mobile;// || window.matchMedia("screen and (max-width: 530px)").matches;
	},
	
	isDesctop: function() 
	{
		return this.setup.screen == ScreenTypes.desctop;
	},
	
	getOSofPlatform: function() 
	{
		var platformId = null;
		
		if(window.cordova) {
			platformId = cordova.platformId;
		}
		
		return OSTypes.getOSType(platformId);
	}
};

var PlatformTypes = 
{
	app: "app",
	browser: "browser"
};

var OSTypes = 
{
	android: "android",
	ios: "ios",
	unknown: "unknown",
	
	getOSType: function(name) {
		if(OSTypes[name]) {
			return OSTypes[name];
		} else {
			return OSTypes.unknown;
		}
	}
};

var ScreenTypes = 
{
	mobile: "mobile",
	desctop: "desctop"
};

var MessagesOrder = 
{
	topBottom: "topBottom",
	bottomTop: "bottomTop"
};

var ConnectorTypes = 
{
	http: "http",
	ws: "ws"
};

var App = 
{
	startUrl: null,
	startGlobalUrl: window.location.href,

	prepare: function()
	{
		ApiClient.UI.getView({}, function(html)
		{
			UIManager.setHTML(getEl("TemplatesHandler"), html);

      EventsManager.fire(Events.Application.appPrepared, null,
			{
        noQ: true
      });
		});
	},
	
	init: function() 
	{
		EventsManager.fire(Events.Application.appInited, null, {
			noQ: true
		});
		
		PageDirector.init();

		UITextManager.init();
		
		Storage.init(AppInfo.setup.dataStorage);
		Session.init(AppInfo.setup.sessionStorage);

		UrlParser.init();
		NavigationManager.init();
		UpdatesWatcher.init();

		AppPage.retrivePages();

		Templater.init();
		Templater.digest(document.body);

		EventsManager.fire(Events.Application.appReady, null, {
			noQ: true
		});
	},
	run: function() 
	{
		EventsManager.subscribe(Events.NavigationManager.urlOpened, function() 
		{
			App._run();
		},
		{
			once: true
		});
		
		NavigationManager.goToUrl(new NavigationManagerUrl().createFromRelative(App.startUrl, App.startGlobalUrl));
	},
	_run: function() 
	{
		EventsManager.fire(Events.Application.appStarting, null, {
			noQ: true
		});

		UpdatesWatcher.run();
		
		EventsManager.fire(Events.Application.appRunning, null, {
			noQ: true
		});
	}
};


Events.Application = 
{
	appPrepared: "appPrepared",
	appInited: "appInited",
	appReady: "appReady",
	appStarted: "appStarted", // compatibility with mobile app
	
	appStarting: "appStarting",
	appRunning: "appRunning",
	appFailedToLoad: "appFailedToLoad",
	
	noConnection: "noConnection",
	updateRequired: "updateRequired",
	serviceUnavailable: "serviceUnavailable",

	resetRequested: "resetRequested"
};

Events.registerEvents(Events.Application);

UITexts.Application = 
{
	updatesCheckingFailed: "updatesCheckingFailed",
	invalidServerResponse: "invalidServerResponse"
};

var AppPage = 
{
	pages: {},
	
	_pagesHTML: {},
	
	retrivePages: function() 
	{
		var pagesHandler = getEl("_pages_handler");
		// TODO: consider a better solution
		// Explanation: because sometimes lazy developers like me decide to create templates inside of page html 
		// and Templater.UI.digest process them as if they are meant to be used as plain html
		TemplatesManager.retrieveTemplates(pagesHandler);
		
		var pages = pagesHandler.querySelectorAll("[data-page]");
		
		performOnElsList(pages, function(page) 
		{
			AppPage._pagesHTML[UIManager.getElData(page, "page")] = UIManager.getHTML(page);
		});
		
		UIManager.removeChildren(pagesHandler);
	},
	
	preparePage: function(pageName) 
	{
		var pageMetaData = MetaData.getForPage(NavigationManager.getCurrentRelativeUrl());
		if(pageMetaData) {
			DocumentTitle.setTitle(pageMetaData.title);
		} else {
			DocumentTitle.setTitle(DocumentTitle.original);
		}
		
		return new AppPage.pages[pageName]();		
	},
	
	getPageHTML: function(pageName) 
	{
		return AppPage._pagesHTML[pageName];
	},
};

var NavigationManager = 
{
	currentUrlInfo: null, //new NavigationManagerUrl('?page=index', "/"),
	urlStack: [],
	currentStackIndex: -1,
	
	init: function(url) 
	{
		//this.urlStack = [url];
		//this.restartNavigator();
	},
	restartNavigator: function(urlInfo, callback) 
	{
		this.urlStack = [];
		this.currentStackIndex = -1;
		
		this.goToUrl(urlInfo, callback);
	},
	setCurrentUrl: function(url) 
	{
		this.urlStack[this.urlStack.length - 1] = url;
	},
	getCurrentUrl: function() 
	{
		var value = Remember.that(Remember.my.currentUrl);
		
		if(value) {
			value = JSON.parse(value);
		} else {
			value = OR(this.currentUrlInfo, new NavigationManagerUrl('?page=index', "/"));
		}
		
		return value;
	},
	pushUrl: function(urlInfo) 
	{
		if(this.urlStack[this.currentStackIndex] != urlInfo) { // if not the same url
			
			if(this.urlStack.length > this.currentStackIndex+1) {
				this.urlStack = this.urlStack.slice(0, this.currentStackIndex+1);
			}
			this.urlStack.push(urlInfo);
			this.currentStackIndex++;
		}
	},
	setUrl: function(urlInfo) 
	{
		this.currentUrlInfo = urlInfo;
		this.setCurrentUrl(urlInfo);
		
		Remember.please(Remember.my.currentUrl, JSON.stringify(urlInfo));
		
		EventsManager.fire(Events.NavigationManager.urlChanged, {
			urlInfo: urlInfo,
			siteUrl: AppInfo.getBaseUrl() + UrlInfo.read(urlInfo.global).pathname
		});
	},
	replaceUrl: function(urlInfo) 
	{
		this.setUrl(urlInfo);
		
		EventsManager.fire(Events.NavigationManager.urlUpdated, {
			urlInfo: urlInfo,
			siteUrl: AppInfo.getBaseUrl() + UrlInfo.read(urlInfo.global).pathname
		});
	},
	goToUrl: function(urlInfo) 
	{
		this.pushUrl(urlInfo);
		
		this.openUrl(urlInfo);
	},
	openUrl: function(urlInfo) 
	{
		this.setUrl(urlInfo);
		
		if(urlInfo.local) {
			UrlParser.setUrlParamsPart(urlInfo.local);
			UrlParser.init();

			PageDirector.open(UrlParser.getPage());
		} else {
			if(window.location.href != urlInfo.global) {
				window.location = urlInfo.global;
			}
		}
		
		EventsManager.fire(Events.NavigationManager.urlOpened, {
			urlInfo: urlInfo,
			siteUrl: AppInfo.getBaseUrl() + UrlInfo.read(urlInfo.global).pathname
		});
	},
	isAtFirstPage: function() 
	{
		return (this.urlStack.length == 1 && this.urlStack[0] == this.currentUrlInfo);
	},
	reload: function(callback) 
	{
		this.openUrl(this.currentUrlInfo, callback);
	},
	getPreviousUrl: function() 
	{
		if(this.urlStack.length > 1) 
		{
			return this.urlStack[this.currentStackIndex - 1];
		}
	},
	goToPreviousUrl: function(callback) 
	{
		if(this.urlStack.length > 1) 
		{
			this.currentStackIndex--;
			this.urlStack.pop();
			this.openUrl(this.urlStack[this.currentStackIndex], callback, true);
		}
	},
	getNextUrl: function() 
	{
		if(this.urlStack.length > this.currentStackIndex+1) 
		{
			return this.urlStack[this.currentStackIndex+1];
		}
	},
	goToNextUrl: function(callback) 
	{
		if(this.urlStack.length > this.currentStackIndex+1) 
		{
			this.currentStackIndex++;
			this.openUrl(this.urlStack[this.currentStackIndex], callback, true);
		}
	},
	goToExternalUrl: function(url) 
	{
		window.open(url, '_system');
	},
	getCurrentRelativeUrl: function() 
	{
		return this.getRelativeUrl(this.getCurrentUrl());
	},
	getRelativeUrl: function(urlInfo) 
	{
		if(urlInfo.global) 
		{
			return StringExt.trimChar(UrlInfo.read(urlInfo.global).pathname, "/");
		}
		
		return null;
	}
};

var NavigationManagerUrl = function(local, global)
{
	this.local = local;
	this.global = global;
	
	this.create = function(local, global) 
	{
		this.local = local;
		this.global = global;
		
		return this;
	};
	
	this.createFromRelative = function(local, global) {
		this.local = local;
		this.global = UrlInfo.read(global).href;
		
		return this;
	};
	
	this.getForHistory = function() {
		return {
			local: this.local,
			global: this.global
		};
	};
};

var UrlInfo = 
{
	read: function(url) 
	{
		var a = document.createElement('a');
		a.href = url;

		return {
			href: a.href,
			host: a.host,
			hostname: a.hostname,
			port: a.port,
			pathname: a.pathname,
			protocol: a.protocol,
			hash: a.hash,
			search: a.search
		};
	}
};


Events.NavigationManager = 
{
	urlChanged: "urlChanged",
	urlUpdated: "urlUpdated",
	urlOpened: "urlOpened",
};

Events.registerEvents(Events.NavigationManager);

var PageDirector = 
{	
	init: function() 
	{

	},
	currentPage: null,
	currentPageName: null,
	open: function(pageName) 
	{
		if(this.currentPageName) 
		{	
			EventsManager.fire(Events.PageDirector.pageUnloaded, {pageName: pageName}, {
				noQ: true
			});
		}
		
		EventsManager.fire(Events.PageDirector.pageToBeLoaded, {pageName: pageName}, {
			noQ: true
		});
		
		DocumentTitle.setTitle(UITexts.Generic.loading);
		

		EventsManager.fire(Events.PageDirector.pageLoading, {
			pageName: pageName,
			pageHTML: AppPage.getPageHTML(pageName)
		}, {
			noQ: true
		});


		EventsManager.fire(Events.PageDirector.pageLoaded, {pageName: pageName});

		this.currentPageName = pageName;
		this.currentPage = AppPage.preparePage(pageName);
		this.currentPage.run();
		
		EventsManager.fire(Events.PageDirector.pageInited, {pageName: pageName});
	},
	back: function() 
	{
		EventsManager.fire(Events.PageDirector.goToPreviousPage);
	},
	showOperationSuccess: function(message, callback) 
	{
		PageAlerts.alert(UITexts.Generic.alert_info, message, callback);
	},
	showOperationError: function(errors, callback) 
	{
		PageAlerts.alert(UITexts.Generic.alert_error, errors, callback);
	},
	documentHasFocus: function() 
	{
		if(document.hasFocus) 
		{
			return document.hasFocus();
		}
		else if(typeof document.hidden !== "undefined")
		{
			return document.hidden;
		}
		
		return false;
	}
};

var DocumentTitle = 
{
	original: document.title,
	title: document.title,

	format: function(parts) 
	{
		var allParts = [UITexts.Generic.projectName].concat(parts);
		
		return allParts.join(" - ");
	},
	setTitle: function(title) 
	{
		document.title = this.title = title;
		
		EventsManager.fire(Events.PageDirector.titleChanged, {
			title: title
		});
	},
	getTitle: function() 
	{
		return this.title;
	},
	setDocumentTitle: function(title) 
	{
		document.title = title;
	},
	getDocumentTitle: function() 
	{
		return document.title;
	},
};

Events.PageDirector = 
{
	pageToBeLoaded: "pageToBeLoaded",
	pageLoading: "pageLoading",
	pageLoaded: "pageLoaded",
	pageInited: "pageInited",
	pageUnloaded: "pageUnloaded",
	
	pageSuccess: "pageSuccess",
	pageError: "pageError",
	
	titleChanged: "titleChanged",
	
	goToPreviousPage: "goToPreviousPage",
};

Events.registerEvents(Events.PageDirector);

var Remember = 
{
	my: {
		uiLang: "uiLang",
		lastAuth: "lastAuth",
		
		currentUrl: "currentUrl",
		
		fontSize: "fontSize",
		themeName: "themeName",
		isSoundEnabled: "isSoundEnabled",
		onlineUsersOrder: "onlineUsersOrder",
		
		chatMessageInput: function(chatId) {
			return "chatMessageInput_"+chatId;
		},
		chatSendOnEnter: "chatSendOnEnter",
		
		pushToken: "pushToken",
		userChatsFilter: "userChatsFilter",
	},
	
	typesSupport: {
		json: {
			to: function(val) {
				return JSON.stringify(val);
			},
			from: function(val, _default) {
				if(val) {
					try {
						return JSON.parse(val);
					} catch(e) {
						
					}
				}
				
				return _default;
			}
		}
	},
	
	types: {
		userChatsFilter: "json"
	},
	
	defaults: {
		userChatsFilter: 
		{
			onlineOnly: false,
			hideInvites: false,
			hideFinished: false
		}
	},
	
	please: function(key, val) 
	{
		if(this.types[key]) {
			val = this.typesSupport[this.types[key]].to(val);
		}
		Storage.save(key, val);
	},
	
	that: function(key, _default) 
	{
		var result = Storage.get(key);
		if(result === "undefined" || result === undefined || result === null) {
			result = OR(_default, Remember.defaults[key]);
			
			Remember.please(key, result);
		}
		else 
		{
			if(this.types[key]) {
				result = this.typesSupport[this.types[key]].from(result, _default);
			}
		}
		
		return result;
	},
	
	forget: function(key) 
	{
		Storage.remove(key);
	}
};

var Session = 
{
	/**
	 * 
	 * @type {Storage}
	 */
	storage: null,
	AUTH: null,
	user: null,
	
	init: function(storage) 
	{
		this.storage = storage;
		this.AUTH = this.storage.get("AUTH");
		
		Debug.info("Initing auth header:" + Session.AUTH);
		
		EventsManager.subscribe(Events.XMLHTTP.beforeSend, function(data) 
		{
			Debug.info("Sending auth header:" + Session.AUTH);
			data.xmlhttp.setRequestHeader("AUTH", Session.AUTH);
		});
		
		EventsManager.subscribe(Events.UpdatesWatcherData.user, function(res) 
		{
			Session.saveUser(res.user);
		});
		
		EventsManager.subscribe(Events.Session.updateUser, function(data) 
		{
			Session.saveUser(data.user);
		});
		
		EventsManager.subscribe(Events.Session.refreshUser, function() 
		{
			UpdatesWatcher.pause(function() 
			{
				UpdatesWatcher.request.setChecksumForProvider(UpdatesWatcher.providers.user, -1);
			});
		});
		
		UpdatesWatcher.dataProviders.registerMulti([
			UpdatesWatcher.providers.user,
			UpdatesWatcher.providers.userNotifications,
			UpdatesWatcher.providers.userKarma,
			UpdatesWatcher.providers.userChatInvites,
			UpdatesWatcher.providers.userChats,
			
			UpdatesWatcher.providers.chatInfo,
			UpdatesWatcher.providers.chatState,
			UpdatesWatcher.providers.chatMessages,
			UpdatesWatcher.providers.chatStrangerInfo,
			UpdatesWatcher.providers.chatStrangerKarmaVote,
		], function() 
		{
			return {
				userId: (Session.userLoaded() ? Session.user.userId : "")
			};
		});
	},
	setAuth: function(auth) 
	{
		this.saveAuth(auth);
		Debug.info("Setting auth header:" + Session.AUTH);
		
		if(auth) 
		{
			EventsManager.fire(Events.Session.userAuthorized, {
				auth: auth
			});
		}
	},
	saveAuth: function(auth) 
	{
		this.AUTH = auth;
		this.storage.save("AUTH", auth);
		
		Remember.please(Remember.my.lastAuth, auth);
	},
	setUser: function(user) 
	{
		this.setAuth(user.AUTH);
		this.saveUser(user);
	},
	saveUser: function(user) 
	{
		this.user = user;
		this.saveAuth(user.AUTH);
		
		EventsManager.fire(Events.Session.userLoaded, {
			user: user
		});
	},
	hasUser: function() 
	{
		return this.AUTH != null && this.AUTH != "null";
	},
	userHasAccount: function() 
	{
		return (this.user && getBool(this.user.isAccountCreated));
	},
	isUserAgreementAccepted: function() 
	{
		return (this.user && getBool(this.user.isServiceAgreementAccepted));
	},
	userLoaded: function() 
	{
		return this.user != null;
	},
	whenUserLoaded: function(func) 
	{
		if(Session.userLoaded()) 
		{
			func(Session.user);
		}
		else 
		{
			EventsManager.subscribe(Events.Session.userLoaded, function(data) 
			{
				func(Session.user);
			}, {
				once: true
			});
		}
	},
	reset: function() 
	{
		this.AUTH = null;
		this.user = null;
		
		this.storage.save("AUTH", "");
		this.storage.remove("AUTH");
	}
};

Events.Session = 
{
	userAuthorized: "userAuthorized",
	userLoaded: "userLoaded",
	updateUser: "updateUser",
	refreshUser: "refreshUser",
};

Events.registerEvents(Events.Session);

var UpdatesWatcher = new function()
{
	var _this = this;
	
	this.status = 
	{
		active: false,
		suspended: false
	};
	
	this.watcher = null;
	
	this.init = function() 
	{	
		UpdatesWatcher.request.setDefaultRequestData();
		UpdatesWatcher.monitor.start();
		
		this.watcher = UpdatesWatcher.watchers.get();
		this.watcher.init();
		
		EventsManager.subscribe(Events.UpdatesWatcherProc.updatesArrived, function(data) 
		{
			try 
			{
				if(!_this.status.active)
					return;
				
				if(data.res.ERRORS) {
					Register.errorHandler.handle(null, data.res.ERRORS);
					UpdatesWatcher.monitor.failsAmount++;
					return;
				}

				if(data.checksums) 
				{
					performOnEveryKey(data.updates, function(dataKey, dataValue) 
					{
						UpdatesWatcher.request.setChecksumForProvider(dataKey, dataValue.checksum);
					});
				}

				var updates = cloneObj(data.updates);
				updates._root = data.res;
				updates._root.data = data.res;

				_this.processUpdates(updates);

				UpdatesWatcher.monitor.failsAmount = 0;
				
				EventsManager.fire(Events.UpdatesWatcherProc.updatesProcessed, null,
				{
					noQ: true
				});

			}
			catch (ex) 
			{
				Register.errorHandler.handle(ex, uiText(UITexts.Application.updatesCheckingFailed));
				UpdatesWatcher.monitor.failsAmount++;
			}
		});
		
		EventsManager.subscribe(Events.PageDirector.pageInited, function() 
		{
			UpdatesWatcher.continue();
		});
		
		EventsManager.subscribe(Events.Application.updateRequired, function() 
		{
			UpdatesWatcher.terminate();
		});
	};
	
	this.run = function() 
	{
		EventsManager.subscribe(Events.UpdatesWatcherProc.connectorReady, function() 
		{
			EventsManager.fire(Events.UpdatesWatcherProc.watcherStarted, null, {
				noQ: true
			});
		},
		{
			id: "UpdatesWatcher.run",
			once: true
		});
		
		_this.status.active = true;
		_this.watcher.start();
	};
	
	this.processUpdates = function(data, forKeys) 
	{
		performOnEveryKey(data, function(dataKey, dataValue) 
		{
			UpdatesWatcher.data[dataKey] = dataValue.data;
			
			APIResponse.process(dataValue, function(dataValue) 
			{
				if(!forKeys || forKeys.indexOf(dataKey)) 
				{
					EventsManager.fire(Events.UpdatesWatcherData[dataKey], dataValue.data, {
						noQ: true
					});
				}
			});
		});
	};
	
	this.reProcessUpdates = function(forKeys) 
	{
		_this.processUpdates(UpdatesWatcher.data, forKeys);
	};
	
	this.suspend = function() 
	{
		if(this.status.suspended == false) 
		{
			this.status.active = false;
			this.status.suspended = true;
			
			EventsManager.fire(Events.UpdatesWatcherProc.watcherSuspended, null, {
				noQ: true
			});

			this.abort();
		}
	};
	
	this.abort = function() 
	{
		EventsManager.fire(Events.UpdatesWatcherProc.requestAborted, null, {
			noQ: true
		});
	};
	
	this.continue = function() 
	{
		if(this.status.suspended == true) 
		{
			this.status.active = true;
			this.status.suspended = false;
			
			EventsManager.fire(Events.UpdatesWatcherProc.watcherResumed, null, {
				noQ: true
			});
		}
	};
	
	this.pause = function(func) 
	{
		this.suspend();
		func();
		this.continue();
	};
	
	this.terminate = function() 
	{
		this.suspend();
		
		EventsManager.fire(Events.UpdatesWatcherProc.watcherTerminated, null, {
			noQ: true
		});
		
		this.watcher.terminate();
	};
	
	this.restart = function()
	{
		// DON'T force recheck inside of the updating process!!!!
		
		this.terminate();
		this.run();
	};
};

UpdatesWatcher.monitor = 
{
	periodMsec: 3500,
	failsAmount: 0,
	timeout: null,
	
	start: function() 
	{
		this.check();
	},
	
	check: function() 
	{
		if(UpdatesWatcher.status.active) 
		{
			var isRunning = UpdatesWatcher.watcher.isRunning();
			
			EventsManager.fire(Events.UpdatesWatcherProc.monitorCheck, 
			{
				isRunning: isRunning
			},
			{
				noQ: true
			});
			
			if(isRunning == false) 
			{
				UpdatesWatcher.run();
			}
		}
		
		this.timeout = Timeout.set(function()
		{
			UpdatesWatcher.monitor.check();
		},
		this.periodMsec * (1 + this.failsAmount));
	}
};

UpdatesWatcher.request = 
{
	data: {},
	
	getChecksumForProvider: function(key) 
	{
		return this.data[key].checksum;
	},
	
	setChecksumForProvider: function(key, checksum) 
	{
		this.data[key].checksum = checksum;
	},
	
	setChecksumForProviders: function(keys, checksum) 
	{
		performOnElsList(keys, function(key) {
			UpdatesWatcher.request.setChecksumForProvider(key, checksum);
		});
	},
	
	setDefaultRequestData: function() 
	{
		performOnEveryKey(UpdatesWatcher.providers, function(key, value) 
		{
			UpdatesWatcher.request.data[key] = {
				checksum: -1
			};
		});
	}
};

UpdatesWatcher.dataProviders = 
{
	list: [],
	
	register: function(provider, func, id) {
		this.list.push({
			provider: provider,
			func: func,
			id: id
		});
	},
	
	registerMulti: function(providers, func, id) 
	{
		performOnElsList(providers, function(key) 
		{
			UpdatesWatcher.dataProviders.register(key, func, id)
		});
	},
	
	unregister: function(id) 
	{
		for (var i = 0; i < this.list.length; i++) 
		{
			var item = this.list[i];
			if(item.id == id) 
			{
				this.list.splice(i, 1);
			}
		}
	},
	
	unregisterMulti: function(ids) 
	{
		performOnElsList(ids, this.unregister);
	}
};

UpdatesWatcher.watchers = 
{
	get: function() 
	{
		if(AppInfo.getConnector() == ConnectorTypes.ws)
		{
			return this.subscriber;
		} 
		
		if(AppInfo.getConnector() == ConnectorTypes.http)
		{
			return this.poller;
		} 
	}
};

UpdatesWatcher.utils = 
{
	generateRequestId: function(prefix) 
	{
		return prefix + "xxx" + generateRundomString(7);
	},
	getRequestData: function() 
	{
		// combine request data
		performOnElsList(UpdatesWatcher.dataProviders.list, function(dataProvider) 
		{
			var reqData = dataProvider.func();
			
			performOnEveryKey(reqData, function(key, val) {
				UpdatesWatcher.request.data[dataProvider.provider][key] = val;
			});
		});
		
		var filteredRequestData = {};
		
		// remove providers without listeners
		performOnEveryKey(UpdatesWatcher.request.data, function(provider, data) 
		{
			if(EventsManager.getListeners(Events.UpdatesWatcherData[provider]).length > 0) 
			{
				filteredRequestData[provider] = data;
			}
		});
		
		return filteredRequestData;
	}
};

UpdatesWatcher.data = 
{
	
};

UpdatesWatcher.providers = 
{
	main: 'main',
	onlineUsers: 'onlineUsers',
	chatsFeed: "chatsFeed",

	user: 'user',
	userNotifications: 'userNotifications',
	userKarma: 'userKarma',
	userChats: 'userChats',
	userChatInvites: 'userChatInvites',

	chatInfo: 'chatInfo',
	chatState: 'chatState',
	chatMessages: 'chatMessages',
	chatStrangerInfo: 'chatStrangerInfo',
	chatStrangerKarmaVote: 'chatStrangerKarmaVote',
};

Events.UpdatesWatcherData = cloneObj(UpdatesWatcher.providers);
Events.UpdatesWatcherData._root = "_root";

Events.registerEvents(Events.UpdatesWatcherData);

Events.UpdatesWatcherProc = 
{
	watcherStarted: "watcherStarted",
	watcherSuspended: "watcherSuspended",
	watcherResumed: "watcherResumed",
	watcherTerminated: "watcherTerminated",
	
	connectorReady: "connectorReady",
	connectionLost: "connectionLost",
	
	updatesArrived: "updatesArrived",
	updatesProcessed: "updatesProcessed",
	
	requestAborted: "requestAborted",
	responseInvaid: "responseInvaid",
	
	monitorCheck: "monitorCheck"
	
};

Events.registerEvents(Events.UpdatesWatcherProc);

UpdatesWatcher.watchers.poller = 
{
	request: null,
	requestId: null,
	abortedRequests: {},
	
	init: function() 
	{
		EventsManager.subscribe(Events.UpdatesWatcherProc.watcherStarted, function() 
		{
			UpdatesWatcher.watchers.poller.load();
		});
		
		EventsManager.subscribe(Events.UpdatesWatcherProc.updatesProcessed, function() 
		{
			if(UpdatesWatcher.status.active) 
			{
				UpdatesWatcher.watchers.poller.load();
			}
		});
		
		EventsManager.subscribe(Events.UpdatesWatcherProc.watcherResumed, function() 
		{
			UpdatesWatcher.watchers.poller.load();
		});
		
		EventsManager.subscribe(Events.UpdatesWatcherProc.requestAborted, function() 
		{
			UpdatesWatcher.watchers.poller.abortedRequests[UpdatesWatcher.watchers.poller.requestId] = true;
			ApiClient.Updates.abortRequest(UpdatesWatcher.watchers.poller.requestId);
			
		});
	},
	start: function() 
	{
		EventsManager.fire(Events.UpdatesWatcherProc.connectorReady, null, {
			noQ: true
		});
	},
	isRunning: function() 
	{
		if(this.request == null || this.request.readyState == 4)
		{
			//alert("Reestablishing updates loading...");

			this.terminate();

			return false;
		}

		return true;
	},
	load: function() 
	{
		if(this.isRunning()) {
			return;
		}

		var request = {
			id: UpdatesWatcher.utils.generateRequestId("HTTP"),
			auth: Session.AUTH,
			request: {
				providers: UpdatesWatcher.utils.getRequestData()
			}
		};
		
		this.requestId = request.id;

		this.request = ApiClient.Updates.getUpdates(request, function(res)
		{
			if(res.aborted || UpdatesWatcher.watchers.poller.abortedRequests[res.id]) 
			{
				delete(UpdatesWatcher.watchers.poller.abortedRequests[res.id]);
				return;
			}
			
			EventsManager.fire(Events.UpdatesWatcherProc.updatesArrived, {
				res: res,
				updates: res.updates,
				checksums: true
			}, {
				noQ: true
			});
		},
		function(ex, xmlhttp) 
		{
			if(xmlhttp.status === 0) // means offline
			{
				EventsManager.fire(Events.UpdatesWatcherProc.connectionLost, null, 
				{
					noQ: true
				});
			}
			else 
			{
				EventsManager.fire(Events.UpdatesWatcherProc.responseInvaid, {
					ex: ex
				}, {
					noQ: true
				});
			}
		});
	},
	terminate: function() 
	{
		if(this.request != null) {
			this.request = Ajax.abourtRequest(this.request);
		}

//		ApiClient.Updates.resetUpdatesRequest(this.lastRequestId, null);
	}
};


UpdatesWatcher.watchers.subscriber = 
{
	status: {
		connected: false,
		subscribed: false
	},
	
	init: function() 
	{
		EventsManager.subscribe(Events.UpdatesWatcherProc.watcherStarted, function() 
		{
			UpdatesWatcher.watchers.subscriber.commands.load();
		});
		
		EventsManager.subscribe(Events.UpdatesWatcherProc.watcherResumed, function() 
		{
			UpdatesWatcher.watchers.subscriber.commands.load();
		});
		
		EventsManager.subscribe(Events.UpdatesWatcherProc.requestAborted, function() 
		{
			UpdatesWatcher.watchers.subscriber.commands.unsubscribe();
		});
		
		EventsManager.subscribe(Events.UpdatesWatcherProc.monitorCheck, function(check) 
		{
			if(check.isRunning && Session.hasUser()) 
			{
				UpdatesWatcher.watchers.subscriber.commands.creepin();
			}
		});
	},
	start: function() 
	{
		UpdatesWatcher.watchers.subscriber.connection.init();
	},
	isRunning: function() 
	{
		return (this.connection.get() && (this.connection.get().readyState==0 || this.connection.get().readyState==1));
	},
	terminate: function() 
	{
		this.connection.get().close();
	},
	
	whenSubscribed: function(func) 
	{
		if(this.status.subscribed) {
			func();
		} else {
			EventsManager.subscribe(Events.UpdatesWatcherSubscriber.subscribed, function() 
			{
				func();
			}, {
				once: true
			});
		}
	}
};

UpdatesWatcher.watchers.subscriber.connection = 
{
	conn: null,
	get: function() 
	{
		return this.conn;
	},
	init: function() 
	{
		this.conn = new WebSocket(AppInfo.getWssUrl() + '?' + new Date().getTime());
		
		this.conn.onopen = function(e) 
		{
			Debug.info("Connection established!");
			
			UpdatesWatcher.watchers.subscriber.status.connected = true;
			
			EventsManager.fire(Events.UpdatesWatcherSubscriber.connected);
		
			EventsManager.fire(Events.UpdatesWatcherProc.connectorReady, 
			{
				watcher: UpdatesWatcher.watchers.subscriber
			}, 
			{
				noQ: true
			});
		};

		this.conn.onmessage = function(e) 
		{
			Debug.info(e.data);
			
			var res = ApiClient.responseProcessor(e.data);
			
			APIResponse.process(res, function(res) 
			{
				switch(res.command) 
				{
					case UpdatesWatcher.watchers.subscriber.commands.list.LOAD:
					case UpdatesWatcher.watchers.subscriber.commands.list.SUBSCRIBE:
					{
						switch(res.command) 
						{
							case UpdatesWatcher.watchers.subscriber.commands.list.LOAD: 
							{
								EventsManager.subscribe(Events.UpdatesWatcherProc.updatesProcessed, function() 
								{
									UpdatesWatcher.watchers.subscriber.commands.subscribe();
								}, {
									once: true
								});
								EventsManager.fire(Events.UpdatesWatcherProc.updatesArrived, 
								{
									res: res.result, 
									updates: res.result.updates,
									checsums: true
								},
								{
									noQ: true
								});
							}
							break;

							case UpdatesWatcher.watchers.subscriber.commands.list.SUBSCRIBE:
							{
								APIResponse.process(res.result, function() 
								{
									UpdatesWatcher.watchers.subscriber.status.subscribed = true;
									EventsManager.fire(Events.UpdatesWatcherSubscriber.subscribed);

									if(res.result.updates) 
									{
										var updates = {};
										updates[res.result.name] = res.result;
										updates[res.result.name].data = res.result.updates;

										EventsManager.fire(Events.UpdatesWatcherProc.updatesArrived, 
										{
											res: res.result, 
											updates: updates,
											checsums: false
										},
										{
											noQ: true
										});
									}
								});
							}
							break;
						}
					}
					break;

					case UpdatesWatcher.watchers.subscriber.commands.list.UNSUBSCRIBE:
					{
						UpdatesWatcher.watchers.subscriber.status.subscribed = false;
						EventsManager.fire(Events.UpdatesWatcherSubscriber.unsubscribed);
					}
					break;
				}
			}, 
			function(res) {
				Register.errorHandler.handle(null, res.command + ": "+ res.ERRORS);
			});
		};
		
		this.conn.onclose = function() 
		{
			Debug.info('Connection closed.');
			
			UpdatesWatcher.watchers.subscriber.status.connected = false;
			
			EventsManager.fire(Events.UpdatesWatcherSubscriber.disconnected);
			
			if(UpdatesWatcher.status.active) 
			{
				EventsManager.fire(Events.Application.noConnection);
			}
		};
	}, 
	send: function(data) 
	{
		if(this.conn && this.conn.readyState === this.conn.OPEN) 
		{
			this.conn.send(data);
		}
	}
};

UpdatesWatcher.watchers.subscriber.commands = 
{
	list: {
		CREEPIN: "CREEPIN",
		LOAD: "LOAD",
		SUBSCRIBE: "SUBSCRIBE",
		UNSUBSCRIBE: "UNSUBSCRIBE",
		PERFORM: "PERFORM",
		
		PING: "PING"
	},
	load: function()
	{
//		this._execute(this._createUserCommand(this.list.LOAD, 
//		{
//			request: {
//				providers: UpdatesWatcher.utils.getRequestData()
//			}
//		}));

		var request = {
			id: UpdatesWatcher.utils.generateRequestId("HTTP"),
			auth: Session.AUTH,
			request: {
				providers: UpdatesWatcher.utils.getRequestData()
			}
		};
		
		ApiClient.Updates.loadUpdates(request, function(res)
		{
			EventsManager.subscribe(Events.UpdatesWatcherProc.updatesProcessed, function() 
			{
				UpdatesWatcher.watchers.subscriber.commands.subscribe();
			}, {
				once: true
			});
			EventsManager.fire(Events.UpdatesWatcherProc.updatesArrived, 
			{
				res: res, 
				updates: res.updates,
				checsums: true
			},
			{
				noQ: true
			});
		});
	},

	creepin: function()
	{
		this._execute(this._createUserCommand(this.list.CREEPIN));
	},
	
	subscribe: function()
	{
		this._execute(this._createUserCommand(this.list.SUBSCRIBE, 
		{
			subscriptions: UpdatesWatcher.utils.getRequestData()
		}));
	},
	unsubscribe: function() 
	{
		this._execute(this._createUserCommand(this.list.UNSUBSCRIBE));
	},
	
	perform: function(commandName, data) 
	{
		var _this = this;
		UpdatesWatcher.watchers.subscriber.whenSubscribed(function() 
		{
			_this._execute(_this._createUserCommand(_this.list.PERFORM, 
			{
				command: {
					name: commandName,
					data: data
				}
			}));
		});
	},
	
	ping: function() 
	{
		this._execute(this._createUserCommand(this.list.PING));
	},
	
	_createUserCommand: function(name, data) 
	{
		var command = {
			command: name,
			data: {
				id: UpdatesWatcher.utils.generateRequestId("WSS"),
				auth: Session.hasUser() ? Session.AUTH : ""
			}
		};
		
		if(data) {
			command.data = copyProps(command.data, data);
		}
		
		return command;
	},
	
	_execute: function(command) 
	{
		UpdatesWatcher.watchers.subscriber.connection.send(JSON.stringify(command));
	}
};

Events.UpdatesWatcherSubscriber = 
{
	connected: "connected",
	loaded: "loaded",
	subscribed: "subscribed",
	unsubscribed: "unsubscribed",
	
	disconnected: "disconnected"
};

Events.registerEvents(Events.UpdatesWatcherSubscriber);

var UserConfig = 
{
	fontSize: 
	{
		get: function() 
		{
			return Remember.that(Remember.my.fontSize, "_100");
		},
		set: function(index) 
		{
			Remember.please(Remember.my.fontSize, index);
		}
	},

  themeName:
    {
      get: function()
      {
        return Remember.that(Remember.my.themeName, "");
      },
      set: function(index)
      {
        Remember.please(Remember.my.themeName, index);
      }
    },
	
	soundEnabled: 
	{
		get: function() 
		{
			return getBool(Remember.that(Remember.my.isSoundEnabled, true));
		},
		set: function(isEnabled) 
		{
			Remember.please(Remember.my.isSoundEnabled, isEnabled);
		}
	}
};

Events.UserConfig = 
{
	fontSizeChanged: "fontSizeChanged",
	soundEnabledChanged: "soundEnabledChanged",
};

Events.registerEvents(Events.UserConfig);

var DateTimeManager = 
{
	// format example 2014-12-19 13:43:55
	getFormattedDate: function (timeString, prefix)
	{
		var value = timeString;

		var prefixPart = "";
		if (prefix) {
			prefixPart = prefix + " ";
		}

		if(timeString) 
		{
			var date = new Date(timeString.replace(/ /g, "T") + ".000Z");
			//var date = new Date(timeString.replace(/-/g, "/") + " UTC");
		//	var date = new Date(Date.UTC(utcDate.getFullYear(),utcDate.getMonth(),utcDate.getDate(),
		//								utcDate.getHours(), utcDate.getMinutes(), utcDate.getSeconds()));

			if(isNaN(date) == false)
			{
        var year = date.getFullYear();
        var month = date.getMonth() + 1;
        var day = date.getDate();

        var hour = date.getHours();
        var minutes = date.getMinutes();
        var seconds = date.getSeconds();

        var datePart = year + "-" + OX(month) + "-" + OX(day);
        var timePart = OX(hour) + ":" + OX(minutes) + ":" + OX(seconds);

        var currentDate = new Date();
        var yesterday = new Date(new Date().setHours(date.getHours() - 24));


        value = datePart + " " + timePart;

        if (currentDate.getTime() - date.getTime() < 1000 * 60 * 60 * 24 * 2)
        {
          var timePassed = currentDate.getTime() - date.getTime() > 1000 * 60 * 60 * 3;

          if (date.getDate() == yesterday.getDate())
          {
            value = timePart;
            if (prefix) {
              prefixPart = prefix + " "+uiText(UITexts.DateTimeManager.yesterdayAt)+" ";
            } else {
              prefixPart = " "+uiText(UITexts.DateTimeManager.yesterday)+" ";
            }
          }

          if (date.getDate() == currentDate.getDate()) {
            value = timePart;
            if (prefix) {
              prefixPart = prefix + " "+uiText(UITexts.DateTimeManager.today)+" ";
            }
          }

          if (timePassed == false) {
            if (prefix)
            {
              prefixPart = prefix + " "+uiText(UITexts.DateTimeManager.todayAt)+" ";
            }
          }
        }
			}
		}

		return prefixPart + value;

	},
	dateDaysDiff: function (d1, d2)
	{
		var t2 = d2.getTime();
		var t1 = d1.getTime();

		return parseInt((t2 - t1) / (24 * 3600 * 1000));
	},
	dateUtcFromString: function(timeString) 
	{
		return new Date(timeString.replace(/-/g, "/") + " UTC");
	}
};

UITexts.DateTimeManager = 
{
	today: "today",
	todayAt: "todayAt",
	yesterday: "yesterday",
	yesterdayAt: "yesterdayAt"
};

var ErrorHandler = function(id) 
{
	var _this = this;
	
	this.id = null;
	this.lastError = null;

	this.init = function() 
	{

	};
	
	this.handle = function(ex, userFriendlyMessage)
	{
		this.lastError = null;
		
		if(ex) {
			Debug.error(ex);
			this.lastError = new Array(ex, ex.message, ex.stack, ex.userData, navigator.userAgent).join("\n");;
		}
		
		EventsManager.fire(Events.Errors.occured, {
			ex: ex,
			message: userFriendlyMessage
		});
	};
	
	this.report = function() 
	{
		if(_this.lastError) 
		{
			ApiClient.User.submitError(_this.lastError, function() {
				EventsManager.fire(Events.Errors.reported);
			});
		}
	};
};


Events.Errors = 
{
	occured: "occured",
	reported: "reported"
};

Events.registerEvents(Events.Errors);

EventsManager.subscribe(Events.Application.appStarting, function() 
{
	Register.errorHandler = new ErrorHandler();
	Register.errorHandler.init();
});

var FontsManager = function()
{
	this.fontClass = null;
	
	this.init = function() 
	{
		this.fontClass = UserConfig.fontSize.get();
		this._applyFontSize(this.fontClass, this.fontClass);
	};
	
	this.changeFontSize = function(newFont)
	{
		this._applyFontSize(this.fontClass, newFont);
		this.fontClass = newFont;
	};
	
	this._applyFontSize = function(oldFont, newFont)
	{
    EventsManager.fire(Events.UserInfo.fontChanged, {
      newFont: newFont,
      oldFont: oldFont,
    });

    UserConfig.fontSize.set(newFont);

    Remember.please(Remember.my.fontSize, newFont);


    UIManager.removeClassFromEl(document.body, oldFont.toString());
    UIManager.addClassToEl(document.body, newFont.toString());

    return true;
	};
};

EventsManager.subscribe(Events.Application.appReady, function() 
{
	Register.fontsManager = new FontsManager();
	Register.fontsManager.init();
});

var ThemesManager = function()
{
	this.themeClass = null;
	
	this.init = function() 
	{
		this.themeClass = UserConfig.themeName.get();
		this._applyFontSize(this.themeClass, this.themeClass);
	};

	this.switchTheme = function(themeName)
	{
		if(this.themeClass == themeName) {
			this.setTheme("");
		} else {
      this.setTheme(themeName);
		}
	};
	
	this.setTheme = function(newTheme)
	{
		this._applyFontSize(this.themeClass, newTheme);
		this.themeClass = newTheme;
	};
	
	this._applyFontSize = function(oldTheme, newTheme)
	{
    EventsManager.fire(Events.UserInfo.themeChanged, {
      newTheme: newTheme,
      oldTheme: oldTheme,
    });

    UserConfig.themeName.set(newTheme);

    Remember.please(Remember.my.themeName, newTheme);


    UIManager.removeClassFromEl(document.body, oldTheme.toString());
    UIManager.addClassToEl(document.body, newTheme.toString());

    return true;
	};
};

EventsManager.subscribe(Events.Application.appReady, function() 
{
	Register.themesManager = new ThemesManager();
	Register.themesManager.init();
});

var LayoutManager = function() 
{
	var _this = this;

	this.headerPadding = 
	{
		apply: function() {
			UIManager.setStyle(_this.parts.middle, "paddingTop", this.getHeaderHeight());
		},
		getHeaderHeight: function(getInt) {
			return UIManager.getElComputedStyle(_this.parts.topBody, "height", getInt);
		},
	};
};

var NotificationTools = 
{
	title: null,
	sound: null,
	
	init: function() 
	{
		this.title = new NotificationTools.title();
		this.title.init();
		
		this.sound = new NotificationTools.sound();
		this.sound.init();
	}
};

NotificationTools.sound = function()
{
	var _this = this;
	
	this.notificationSound = null;
	
	this.init = function() 
	{
		this.notificationSound = document.createElement("audio");
		this.notificationSound.setAttribute("src", "/public/sounds/New/message_notification_lover.mp3");
		this.notificationSound.setAttribute("id", "NotificationSound");
		document.body.appendChild(this.notificationSound);
		
		EventsManager.subscribe(Events.UserNotifications.newNotifications, function() 
		{
			Threader.putInQueue(function() {
				_this.play();
			});
		});
	};
	
	this.play = function() 
	{
		if(PageDirector.documentHasFocus() == true)
			return;
		
		if(UserConfig.soundEnabled.get()) 
		{
			this.notificationSound.play();
		}
	};
};

NotificationTools.title = function()
{
	var _this = this;
	
	this.favicon = document.getElementById("Favicon");
	
	this.init = function() 
	{
		EventsManager.subscribe(Events.UserNotifications.updated, function(data) 
		{
			_this.setTabTitle(data.total);
		});
		
		EventsManager.subscribe(Events.PageDirector.titleChanged, function() {
			EventsManager.refire(Events.UserNotifications.updated);
		});
	};
	
	this.setTabTitle = function(notifAmount) 
	{
		DocumentTitle.setDocumentTitle(notifAmount > 0 ? ("(" + notifAmount + ") " + DocumentTitle.title) : DocumentTitle.title);
		
		this.favicon.href = notifAmount > 0 ? AppInfo.setup.iconAlt : AppInfo.setup.iconMain;
	};
};

EventsManager.subscribe(Events.Application.appReady, function() 
{
	NotificationTools.init();
});

/**
 * 
 * @type type
 */
var UrlParser = 
{
	url: null,
	baseUrlPath: null,
	urlParamsPart: null,
	urlParams: {},
	
	init: function() 
	{
		if(!this.url) {
			this.url = document.location.search;
		}
		
		var parts = this.url.split('?');
		
		this.baseUrlPath = parts[0];
		this.urlParamsPart = parts.length > 1 ? parts[1] : "";
		
		this.parseUrl();
	},
	parseUrl: function() 
	{
		this.urlParams = {};
		
		var queries = this.urlParamsPart.split('&');

		for (var i = 0; i < queries.length; i++) 
		{
			var split = queries[i].split('=');
			this.urlParams[split[0]] = OR(split[1], "");
		}
	},
	getParam: function(key, _defalt) 
	{
		return this.urlParams[key] !== undefined ? this.urlParams[key] : _defalt;
	},
	setUrlParamsPart: function(params) 
	{
		this.url = this.baseUrlPath + params;
		this.parseUrl();
	},
	getPage: function(defaultPage) 
	{
		var page = UrlParser.getParam("page");

		if(!defaultPage) {
			defaultPage = 'index';
		}

		if(!page) {
			page = defaultPage;
		}

		return page;
	}
};

var PageAlerts = 
{
	alert: function(title, message, callback) 
	{
		if(navigator.notification && navigator.notification.alert) 
		{
			navigator.notification.alert(message, callback, title, "OK");
		}
		else 
		{
			window.alert(message);
			
			if(callback) {
				callback();
			}
		}
	},
	confirm: function(message, callback) 
	{
		if(navigator.notification && navigator.notification.confirm) 
		{
			navigator.notification.confirm(message, function(buttonIndex) {
				if(buttonIndex === 1) {
					callback();
				}
			}, UITexts.Generic.alert_confirm);
		}
		else 
		{
			if(window.confirm(message)) 
			{
				if(callback) {
					callback();
				}
			}
		}
	},
	prompt: function(title, defaultText, callback) 
	{
		if(navigator.notification 
		&& navigator.notification.prompt 
		&& AppInfo.getOSofPlatform() != OSTypes.android) 
		{
			navigator.notification.prompt("", function(result) {
				if(result.buttonIndex === 1) {
					callback(result.input1);
				}
			}, title, null, defaultText);
		}
		else 
		{
			var text = window.prompt(title, defaultText);
			
			if(callback) {
				callback(text);
			}
		}
	}
}

var MetaData = 
{
	data: {},
	load: function(el) 
	{
		this.data = JSON.parse(el);
	},
	getForPage: function(page, _useDefault) 
	{
		if(page === "") {
			page = "index/index";
		}
		
		if(this.data[page]) {
			return this.data[page];
		} 
		
		if(_useDefault) 
		{
			var _defaultPage = "index/index";

			if(this.data[_defaultPage]) {
				return this.data[_defaultPage];
			}
		}
		
		return null;
	},
	formatTitleForPage: function(page, data) 
	{
		return new Temparser(this.getForPage(page).title, data, "MetaData_"+page).format();
	}
};

var DataValidator = 
{
	tag: function(tag) 
	{
		var result = new DataValidatorResult();
		
		var tag = UIFormat.prepareTag(tag);
		
		if(tag.length > 0) 
		{
			if(tag.length < 3) 
			{
				result.message = uiText(UITexts.DataValidator.tagTooShort);
			}
			else 
			{
				if(tag.length > 20) 
				{
					result.message = uiText(UITexts.DataValidator.tagTooLong);
				}
				else 
				{
					result.valid = true;
				}
			}
		}
		else 
		{
			result.message = uiText(UITexts.DataValidator.tagIsEmpty);
		}
		
		return result;
	},
	
	feedback: function(feedback) 
	{
		var result = new DataValidatorResult();
		
		if(feedback.length > 50) 
		{
			result.message = uiText(UITexts.DataValidator.feedbackTooLong);
		}
		else 
		{
			result.valid = true;
		}
		
		return result;
	},
	
		
	complain: function(complain) 
	{
		var result = new DataValidatorResult();
		
		if(complain.length > 500) 
		{
			result.message = uiText(UITexts.DataValidator.complainTooLong);
		}
		else 
		{
			result.valid = true;
		}
		
		return result;
	},
};

var DataValidatorResult = function()
{
	this.valid = false;
	this.message = null;
};


UITexts.DataValidator = 
{
	tagTooShort: "tagTooShort",
	tagTooLong: "tagTooLong",
	tagIsEmpty: "tagIsEmpty",
	
	feedbackTooLong: "feedbackTooLong",
	
	complainTooShort: "complainTooShort",
	complainTooLong: "complainTooLong",
	
	fileFormatInvalid: "fileFormatInvalid",
	fileNotSelected: "fileNotSelected"
};

var DataCache = 
{
	data: {},
	
	get: function(key, _default) 
	{
		if(this.data[key] === undefined) {
			this.data[key] = cloneObj(_default);
		}
		
		return this.data[key];
	},
	save: function(key, value) 
	{
		this.data[key] = cloneObj(value);
	},
	remove: function(key) 
	{
		delete this.data[key];
	}
};

DataCache.Keys = 
{
	usersSearchTags: "usersSearchTags"
};

var UI = {};

UI.Modal = function(handler)
{
	var _this = this;
	this.handler = handler;
	this.modal = handler.querySelector(".UI-Modal");
	this.content = this.modal.querySelector("._content");
	this.closeButtons = this.modal.querySelectorAll("._close");
	
	this.init = function() 
	{
		UIManager.addEvent(this.closeButtons, "click", function() {
			_this.dissmiss();
		});
		
		UIManager.addEvent(window.document, "keydown", function(event) {
			if(event.keyCode === 27) {
				_this.dissmiss();
			}
		});
	};
	
	this.showHTML = function(html) 
	{
		UIManager.setHTML(this.content, html);
		UIManager.showEl(this.modal);
	};
	
	this.showEl = function(el) {
		UIManager.setChild(this.content, el);
		UIManager.showEl(this.modal);
	};
	
	this.dissmiss = function() 
	{
		UIManager.hideEl(this.modal);
	};
};

EventsManager.subscribe(Events.Application.appStarting, function() 
{
	Register.mainModal = new UI.Modal(getEl("MainModal"));
	Register.mainModal.init();
});

var ApiClient = new function () 
{
	this.formatAPIv2Url = function(part) 
	{
		return AppInfo.getBaseUrl() + "/api_v2/" + part + "?" + this._formatUrlParams();
	};
	
	this._formatUrlParams = function() 
	{
		var params = {
			platform: AppInfo.getPlatform(),
			OS: AppInfo.getOS(),
      // TODO: remove this check after fixing order of app initialization
			lang: Storage.storage ? Remember.that(Remember.my.uiLang, document.documentElement.lang) : "unknown",
			version: AppInfo.getVersion()
		};
		
		return joinObject(params, "=", "&");
	};
};

ApiClient.responseProcessor = function(response) 
{
	try 
	{
		response = JSON.parse(response);	
		
		EventsManager.fire(Events.ApiClient.response, response);
		
		return response;
	}
	catch (e) 
	{
		throw "Invalid API response. Details: " + e;
	}
	
};

var APIResponse = 
{
	process: function(res, ifOk, ifNotOK) 
	{
		if(res.CODE == 'OK') {
			if(ifOk) {
				ifOk(res);
			}
		} else {
			if(ifNotOK) {
				ifNotOK(res);
			}
		}
	}
};

Events.ApiClient = 
{
	response: "response"
};

Events.registerEvents(Events.ApiClient);


ApiClient.Auth = 
{
	getAuth: function(callback) 
	{
		return Ajax.Invoke({
			type: "GET",
			url: ApiClient.formatAPIv2Url("auth/getAuth/"),
			processor: ApiClient.responseProcessor
			
		}, callback);
	},
	
	createAccount: function(accountData, callback) 
	{
		return Ajax.Invoke({
			url: ApiClient.formatAPIv2Url("auth/createAccount"),
			type: "POST",
			data: accountData,
			processor: ApiClient.responseProcessor,
			
		}, callback);
	},
	
	login: function(loginData, callback) 
	{
		return Ajax.Invoke({
			url: ApiClient.formatAPIv2Url("auth/login"),
			type: "POST",
			data: loginData,
			processor: ApiClient.responseProcessor,
			
		}, callback);
	},
	
	restoreAccess: function(accountData, callback) 
	{
		return Ajax.Invoke({
			url: ApiClient.formatAPIv2Url("auth/restoreAccess"),
			type: "POST",
			data: accountData,
			processor: ApiClient.responseProcessor
			
		}, callback);
	},
	
	logout: function(callback) 
	{
		return Ajax.Invoke({
			url: ApiClient.formatAPIv2Url("auth/logout"),
			type: "GET",
			
		}, callback);
	},
}

ApiClient.Chat = 
{
  create: function(data, callback)
  {
    return Ajax.Invoke({
      type: "POST",
      url: ApiClient.formatAPIv2Url("chat/create/"),
      processor: ApiClient.responseProcessor,
      data: data

    }, callback);
  },

  join: function(chatId, callback)
  {
    return Ajax.Invoke({
      type: "POST",
      url: ApiClient.formatAPIv2Url("chat/join/" + chatId),
      processor: ApiClient.responseProcessor,

    }, callback);
  },

	inviteUserToChat: function(userId, data, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("chat/invite/" + userId),
			processor: ApiClient.responseProcessor,
			data: data
				
		}, callback);
	},
	
	getChat: function(chatId, callback) 
	{
		return Ajax.Invoke({
			type: "GET",
			url: ApiClient.formatAPIv2Url("chat/getChat/" + chatId),
			processor: ApiClient.responseProcessor
			
		}, callback);
	},
	
	acceptInvitation: function(chatId, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("chat/acceptInvitation/" + chatId),
			processor: ApiClient.responseProcessor,
			
		}, callback);
	},
	
	rejectInvitation: function(chatId, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("chat/rejectInvitation/" + chatId),
			processor: ApiClient.responseProcessor,
			
		}, callback);
	},
	
	complainOnChat: function(chatId, data, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("chat/complain/" + chatId),
			processor: ApiClient.responseProcessor,
			data: data
			
		}, callback);
	},
	
	chatHistory: function(chatId, startingFrom, amount, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("chat/history/" + chatId + "/" + startingFrom + "/" + amount),
			processor: ApiClient.responseProcessor
			
		}, callback);
	},
	
	sendMessage: function (chatId, message, callback, errorCallback)
	{	
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("chat/sendMessage/" + chatId),
			processor: ApiClient.responseProcessor,
			data:{
				message: message,
			}
			
		}, callback, errorCallback);
	},
	
	sendTypingState: function(chatId, state, callback) 
	{
		return Ajax.Invoke({
			type: "GET",
			url: ApiClient.formatAPIv2Url("chat/sendTypingState/" + chatId + "/" + state),
			processor: ApiClient.responseProcessor,
			
		}, callback);
	},
	
	setKarvaVote: function (chatId, value, callback)
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("chat/setKarvaVote/" + chatId + "/" + value),
			processor: ApiClient.responseProcessor
			
		}, callback);
	},
	
	leaveKarvaFeedback: function (chatId, data, callback)
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("chat/leaveKarvaFeedback/" + chatId),
			processor: ApiClient.responseProcessor,
			data: data
			
		}, callback);
	},
	
	setCustomName: function(chatId, name, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("chat/setCustomName/" + chatId),
			processor: ApiClient.responseProcessor,
			data: {
				chatName: name
			}
			
		}, callback);
	},
	
	setAcceptMedia: function(chatId, accept, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("chat/setAcceptMedia/" + chatId + "/" + accept),
			processor: ApiClient.responseProcessor
			
		}, callback);
	},
	
	setAccess: function(chatId, access, callback) 
	{
		return Ajax.Invoke({
			type: "GET",
			url: ApiClient.formatAPIv2Url("chat/setAccess/" + chatId + "/" + access),
			processor: ApiClient.responseProcessor
			
		},callback);
	},

	requestAccessChange: function(chatId, access, callback)
	{
    return Ajax.Invoke({
      type: "GET",
      url: ApiClient.formatAPIv2Url("chat/requestAccessChange/" + chatId + "/" + access),
      processor: ApiClient.responseProcessor

    },callback);
	},

  acceptAccessChange: function(chatId, callback)
  {
    return Ajax.Invoke({
      type: "GET",
      url: ApiClient.formatAPIv2Url("chat/acceptAccessChange/" + chatId),
      processor: ApiClient.responseProcessor

    },callback);
  },

  declineAccessChange: function(chatId, callback)
  {
    return Ajax.Invoke({
      type: "GET",
      url: ApiClient.formatAPIv2Url("chat/declineAccessChange/" + chatId),
      processor: ApiClient.responseProcessor

    },callback);
  },

  cancelAccessChange: function(chatId, callback)
  {
    return Ajax.Invoke({
      type: "GET",
      url: ApiClient.formatAPIv2Url("chat/cancelAccessChange/" + chatId),
      processor: ApiClient.responseProcessor

    },callback);
  },
	
	getMessagesHistoryOlderThan: function(chatId, offset, amount, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("chat/getMessagesHistoryOlderThan/"+chatId+"/"+offset+"/"+amount),
			processor: ApiClient.responseProcessor,
			
		}, callback);
	},
	
	disconnect: function (chatId, callback)
	{		
		return Ajax.Invoke({
			type: "GET",
			url: ApiClient.formatAPIv2Url("chat/disconnect/"+chatId),
			processor: ApiClient.responseProcessor,
			
		}, callback);
	}
};


ApiClient.Main = 
{
	getVersion: function(callback) 
	{
		return Ajax.Invoke({
			type: "GET",
			url: ApiClient.formatAPIv2Url("main/getVersion"),
			processor: ApiClient.responseProcessor
		
		}, callback);
	},
	setUILang: function(lang, callback) 
	{
		return Ajax.Invoke({
			type: "GET",
			url: ApiClient.formatAPIv2Url("main/setUILang/"+lang),
			processor: ApiClient.responseProcessor,
			
		}, callback);
	},
	
	acceptServiceAgreement: function(callback) 
	{
		return Ajax.Invoke({
			type: "GET",
			url: ApiClient.formatAPIv2Url("main/acceptServiceAgreement"),
			processor: ApiClient.responseProcessor
			
		}, callback);
	},
	
	getPopularTags: function(startingFrom, amount, callback) 
	{
		return Ajax.Invoke({
			type: "GET",
			url: ApiClient.formatAPIv2Url("main/getPopularTags/"+startingFrom+"/"+amount),
			processor: ApiClient.responseProcessor
			
		}, callback);
	},
	
	getOnlineUsers: function(callback) 
	{
		return Ajax.Invoke({
			type: "GET",
			url: ApiClient.formatAPIv2Url("main/getOnlineUsers"),
			processor: ApiClient.responseProcessor
			
		}, callback);
	},

  getChatsFeed: function(callback)
  {
    return Ajax.Invoke({
      type: "GET",
      url: ApiClient.formatAPIv2Url("main/getChatsFeed"),
      processor: ApiClient.responseProcessor

    }, callback);
  },
	
	getUser: function(userId, callback) 
	{
		return Ajax.Invoke({
			url: ApiClient.formatAPIv2Url("main/getUser/" + userId),
			type: "GET",
			processor: ApiClient.responseProcessor,
			
		}, callback);
	},
	
	getUserByName: function(userName, callback) 
	{
		return Ajax.Invoke({
			url: ApiClient.formatAPIv2Url("main/getUserByName/" + userName),
			type: "GET",
			processor: ApiClient.responseProcessor,
			
		}, callback);
	},
	
	getRandomUser: function(callback) 
	{
		return Ajax.Invoke({
			url: ApiClient.formatAPIv2Url("main/getRandomUser"),
			type: "GET",
			processor: ApiClient.responseProcessor
			
		}, callback);
	},
	
	getUserKarmaHistory: function(userId, callback) 
	{
		return Ajax.Invoke({
			url: ApiClient.formatAPIv2Url("main/getUserKarmaHistory/" + userId),
			type: "GET",
			processor: ApiClient.responseProcessor,
			
		},	callback);
	},
	
	getUserChats: function(userId, callback) 
	{
		return Ajax.Invoke({
			url: ApiClient.formatAPIv2Url("main/getUserChats/" + userId),
			type: "GET",
			processor: ApiClient.responseProcessor,
			
		},	callback);
	},

  getUserActiveAccounts: function(userId, callback)
  {
    return Ajax.Invoke({
      url: ApiClient.formatAPIv2Url("main/getUserActiveAccounts/" + userId),
      type: "GET",
      processor: ApiClient.responseProcessor,

    },	callback);
  },
	
	getPublicChat: function(chatId, callback) 
	{
		return Ajax.Invoke({
			type: "GET",
			url: ApiClient.formatAPIv2Url("main/getPublicChat/" + chatId),
			processor: ApiClient.responseProcessor
			
		}, callback);
	},
	
	getPublicChatHistory: function(chatId, startingFrom, amount, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("main/getPublicChatMessagesHistory/" + chatId + "/" + startingFrom + "/" + amount),
			processor: ApiClient.responseProcessor
			
		}, callback);
	},
	
	getPublicChatMessage: function(messageId, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("main/getPublicChatMessage/" + messageId),
			processor: ApiClient.responseProcessor
			
		}, callback);
	},
	
	findUsers: function(searchQuery, startingFrom, amount, callback) 
	{
		return Ajax.Invoke({
			url: ApiClient.formatAPIv2Url("main/findUsers/" + startingFrom + "/" + amount),
			type: "POST",
			data: {
				query: JSON.stringify(searchQuery)
			},
			processor: ApiClient.responseProcessor,
			
		}, callback);
	},
};


ApiClient.UI = 
{
	getLayout: function(params, callback) 
	{
		return Ajax.Invoke({
			type: "GET",
			url: ApiClient.formatAPIv2Url("UI/getLayout/"+params.component+"/"+params.version)
			
		}, callback);
	},

  getView: function(params, callback)
  {
    return Ajax.Invoke({
      type: "GET",
      url: ApiClient.formatAPIv2Url("UI/getView/"+params.component+"/"+params.version)

    }, callback);
  },
	
	getCSS: function(params, callback) 
	{
		return Ajax.Invoke({
			type: "GET",
			url: ApiClient.formatAPIv2Url("UI/getCSS/"+params.component+"/"+params.version)
			
		}, callback);
	},
	
	getJS: function(params, callback) 
	{
		return Ajax.Invoke({
			type: "GET",
			url: ApiClient.formatAPIv2Url("UI/getJS/"+params.component+"/"+params.version)
			
		}, callback);
	}
};


ApiClient.Updates = 
{
	loadUpdates: function(request, callback, errorCallback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("updates/loadUpdates/" + request.id),
			data: {
				request: JSON.stringify(request)
			},
			processor: ApiClient.responseProcessor
			
		}, callback, errorCallback);
	},
	
	getUpdates: function(request, callback, errorCallback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("updates/getUpdates/" + request.id),
			data: {
				request: JSON.stringify(request)
			},
			processor: ApiClient.responseProcessor
			
		}, callback, errorCallback);
	},
	
	abortRequest: function(requestId, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("updates/abortRequest/" + requestId),
			processor: ApiClient.responseProcessor,
			
		}, callback);
	}
};


ApiClient.User = 
{
	setUserLangs: function(langs, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("user/setUserLangs"),
			processor: ApiClient.responseProcessor,
			data: {
				request: JSON.stringify({langs: langs})
			}
			
		}, callback);
	},
	
	getUserInfo: function(callback) 
	{
		return Ajax.Invoke({
			type: "GET",
			url: ApiClient.formatAPIv2Url("user/info"),
			processor: ApiClient.responseProcessor
			
		},callback);
	},
	
	registerPushToken: function(platform, data, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("user/registerPushToken"),
			processor: ApiClient.responseProcessor,
			data: {
				platform: platform,
				data: data
			}
			
		}, callback);
	},
	
	unregisterPushToken: function(platform, data, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("user/unregisterPushToken"),
			processor: ApiClient.responseProcessor,
			data: {
				platform: platform,
				data: data
			}
			
		}, callback);
	},
	
	addNewTag: function(tag, kind, lang, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("user/addNewTag"),
			processor: ApiClient.responseProcessor,
			data: {
				tag: tag,
				kind: kind,
				lang: lang
			}
			
		}, callback);
	},
	
	setUserTagsSettings: function(tags, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("user/setUserTagsSettings"),
			processor: ApiClient.responseProcessor,
			data: { 
				request: JSON.stringify({ tags: tags })
			}
			
		}, callback);
	},
	
	setUserFilterSettings: function(filterSettings, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("user/setUserFilterSettings"),
			processor: ApiClient.responseProcessor,
			data: {
				request: JSON.stringify(filterSettings)
			}
			
		}, callback);
	},
	
	
	/* ======================
	 *	ACCOUNT METHODS START 
	 * ====================== */
	
	updateAccountInfo: function(accountData, callback) 
	{
		return Ajax.Invoke({
			url: ApiClient.formatAPIv2Url("user/updateAccountInfo"),
			type: "POST",
			data: accountData,
			processor: ApiClient.responseProcessor,
			
		}, callback);
	},
	
	setAccountAbout: function(data, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("user/setAccountAbout"),
			processor: ApiClient.responseProcessor,
			data: data
			
		}, callback);
	},
	
	setAccountChattingSettings: function(data, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("user/setAccountChattingSettings"),
			processor: ApiClient.responseProcessor,
			data: data
			
		}, callback);
	},
	
	setAccountEmailSettings: function(data, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("user/setAccountEmailSettings"),
			processor: ApiClient.responseProcessor,
			data: data
			
		}, callback);
	},
	
	setAccountNotificationSettings: function(data, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("user/setAccountNotificationSettings"),
			processor: ApiClient.responseProcessor,
			data: data
			
		}, callback);
	},
	
	switchIsOpenForChat: function(callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("user/switchIsOpenForChat"),
			processor: ApiClient.responseProcessor
			
		}, callback);
	},
	
	removeVote: function(id, callback) 
	{
		return Ajax.Invoke({
			type: "GET",
			url: ApiClient.formatAPIv2Url("/user/removeVote/" + id),
			processor: ApiClient.responseProcessor
			
		}, callback);
	},

  getUploads: function(startingFrom, amount, callback)
  {
    return Ajax.Invoke({
      type: "GET",
      url: ApiClient.formatAPIv2Url("user/getUploads/" + startingFrom + "/" + amount),
      processor: ApiClient.responseProcessor

    }, callback);
  },
	
	uploadImage: function(formData, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("user/uploadImage"),
			processor: ApiClient.responseProcessor,
			data: null,
			formData: formData
			
		}, callback);
	},
	
	submitError: function(error, callback) 
	{
		return Ajax.Invoke({
			type: "POST",
			url: ApiClient.formatAPIv2Url("user/submitError"),
			processor: ApiClient.responseProcessor,
			data: {
				message: error
			}
			
		}, callback);
	},
	
	acceptServiceAgreement: function(callback) 
	{
		return Ajax.Invoke({
			url: ApiClient.formatAPIv2Url("user/acceptServiceAgreement"),
			type: "POST",
			processor: ApiClient.responseProcessor
			
		}, callback);
	},
	
	deleteAccount: function(callback) 
	{
		return Ajax.Invoke({
			url: ApiClient.formatAPIv2Url("user/deleteAccount"),
			type: "POST",
			processor: ApiClient.responseProcessor
			
		}, callback);
	},
};

/**
 * 
 * @type type
 */
AppPage.pages.login = function()
{
	this.run = function() 
	{
		Register.login = function (formData) 
		{
			ApiClient.Auth.login(formData, function(res) 
			{
				APIResponse.process(res, function(res) 
				{
					EventsManager.fire(Events.PageLogin.authSuccessful);

					UpdatesWatcher.pause(function() 
					{
						Session.setUser(res.user);
					
						EventsManager.subscribe(Events.UpdatesWatcherData._root, function() 
						{
							NavigationManager.restartNavigator(new NavigationManagerUrl().createFromRelative("?page=index", "/"));
						}, 
						{
							once: true
						});
					});
					
				}, function(res) 
				{
					PageDirector.showOperationError(res.ERRORS);
				});
			});
		};
	};
};

Events.PageLogin = {
	authSuccessful: "authSuccessful",
	authFailed: "authFailed",
};

Events.registerEvents(Events.PageLogin);

/**
 * 
 * @type type
 */
AppPage.pages.restore_access = function()
{
	this.run = function() 
	{
		Register.restoreAccess = function(formData) 
		{
			ApiClient.Auth.restoreAccess(formData, function(res) 
			{
				 APIResponse.process(res, function(res) 
				 {
					 PageDirector.showOperationSuccess(res.MESSAGE, function() {
						 NavigationManager.goToUrl(new NavigationManagerUrl().createFromRelative("?page=login", "/auth/login"));
					 });
				 }, function(res) 
				 {
					 PageDirector.showOperationError(res.ERRORS);
				 });
			});
		};
	}
};


/**
 * 
 * @type type
 */
AppPage.pages.logout = function()
{
	this.run = function() 
	{
		EventsManager.subscribe(Events.PageDirector.pageInited, function() 
		{
			UpdatesWatcher.terminate();
		
			ApiClient.Auth.logout(function(res) 
			{
				Session.reset();

				EventsManager.subscribe(Events.UpdatesWatcherData._root, function() 
				{
					NavigationManager.restartNavigator(new NavigationManagerUrl().createFromRelative("?page=index", "/"));
				}, 
				{
					once: true
				});

				UpdatesWatcher.run();
			});
		},
		{
			once: true
		});
	};
};

/**
 * 
 * @type type
 */
AppPage.pages.signup = function()
{
	this.run = function() 
	{
		Register.signup = function(formData) 
		{
			ApiClient.Auth.createAccount(formData, function(res) 
			{
				APIResponse.process(res, function(res) 
				{
					UpdatesWatcher.pause(function() 
					{
						Session.setUser(res.user);
						NavigationManager.goToUrl(new NavigationManagerUrl().createFromRelative("?page=index", "/"));
					});
					
				}, function(res) 
				{
					PageDirector.showOperationError(res.ERRORS);
				});
			});
		};
	};
};

/**
 * 
 * @type type
 */
AppPage.pages.chat = function()
{
	var _this = this;
	
	this.run = function() 
	{
		if(EventsManager.wasFired(Events.Application.appStarting)) 
		{
			UpdatesWatcher.suspend();
			
			_this._startChat();
		}
		else 
		{
			EventsManager.subscribeOnce(Events.Application.appStarting, function() 
			{
				_this._startChat();
			});
		}
		
		EventsManager.subscribeOnce(Events.PageDirector.pageUnloaded, function() 
		{
			UpdatesWatcher.suspend();
			
			if(Register.chat) 
			{
				Register.chat.resetChat();
				Register.chat = null;
			}
		});
	};
	
	this._startChat = function() 
	{
		Register.chat = new UserChat(getEl("ChatHandler"));
		Register.chat.init();
		Register.chat.startChatWithId(UrlParser.getParam("chatId"));
	};
};

/**
 * 
 * @type type
 */
AppPage.pages.chat_complain = function()
{
	this.setTitle = function() {};
	
	this.run = function() 
	{
		ApiClient.Chat.getChat(UrlParser.getParam("chatId"), function(res) 
		{
			APIResponse.process(res, function(res) 
			{
				DocumentTitle.setTitle(MetaData.formatTitleForPage("user_chats/complain", {
					NAME: res.chat.nameCustom
				}));
				
				EventsManager.fire(Events.ChatComplainPage.chatInfoLoaded, {
					chat: res.chat
				});
			},
			function() 
			{
				EventsManager.fire(Events.PageDirector.pageError, {
					message: res.ERRORS
				});
			});
		});
	};
};

Events.ChatComplainPage = {
	chatInfoLoaded: 'chatInfoLoaded'
};

Events.registerEvents(Events.ChatComplainPage);

/**
 * 
 * @type type
 */
AppPage.pages.chat_history = function()
{
	var _this = this;
	
	this.chatId = UrlParser.getParam("chatId");
	
	this.run = function() 
	{
		Session.whenUserLoaded(function(user) 
		{
			ApiClient.Chat.getChat(_this.chatId, function(res) 
			{
				APIResponse.process(res, function(res) 
				{
					DocumentTitle.setTitle(MetaData.formatTitleForPage("user_chats/history", {
						NAME: res.chat.nameCustom
					}));

					EventsManager.fire(Events.PageChatHistory.chatInfoLoaded, {
						chat: res.chat
					});

					Register.chatHistory = new ChatHistory(res.chat, Session.user);
					Register.chatHistory.setProvider(function(startingFrom, amountPerPage, callback) 
					{
						ApiClient.Chat.chatHistory(_this.chatId, startingFrom, amountPerPage, function(res) 
						{
							APIResponse.process(res, function(res) 
							{
								callback(res);
							},
							function(res) 
							{
								PageDirector.showOperationError(res.ERRORS);
							});
						});
					});

					Register.chatHistory.pageUrl = new NavigationManagerUrl().create(
							  "?page=chat_history&chatId="+_this.chatId+"&", 
							  "/user_chats/history/"+_this.chatId+"?");

					Register.chatHistory.show();
				},
				function(res) 
				{
					EventsManager.fire(Events.PageDirector.pageError, {
						message: res.ERRORS
					});
				});
			});
		});
	};
};

Events.PageChatHistory = {
	chatInfoLoaded: "chatInfoLoaded"
};

Events.registerEvents(Events.PageChatHistory);

/**
 * 
 * @type type
 */
AppPage.pages.public_chat = function()
{
	var _this = this;
	
	this.chatId = UrlParser.getParam("chatId");
	
	this.run = function() 
	{
		ApiClient.Main.getPublicChat(_this.chatId, function(res)
		{
			APIResponse.process(res, function(res) 
			{
				DocumentTitle.setTitle(MetaData.formatTitleForPage("info/public_chat", {
					NAME: res.chat.name
				}));
				
				EventsManager.fire(Events.PagePublicChat.chatInfoLoaded, {
					chat: res.chat
				});
				
				if(res.chat.canAccessMessages.CODE == 'OK')
				{
					Register.chatHistory = new ChatHistory(res.chat, res.chat.invitedUserInfo);
					Register.chatHistory.setProvider(function(startingFrom, amountPerPage, callback) 
					{
						ApiClient.Main.getPublicChatHistory(_this.chatId, startingFrom, amountPerPage, function(res) 
						{
							APIResponse.process(res, function(res) 
							{
								callback(res);
							},
							function(res) 
							{
								PageDirector.showOperationError(res.ERRORS);
							});
						});
					});

					Register.chatHistory.pageUrl = new NavigationManagerUrl().create(
							  "?page=public_chat&chatId="+_this.chatId+"&", 
							  "/pc/"+_this.chatId+"?");

					Register.chatHistory.show();
				}
				else 
				{
					
				}
			},
			function(res) 
			{
				EventsManager.fire(Events.PageDirector.pageError, {
					message: res.ERRORS
				});
			});
		});
	};
};

Events.PagePublicChat = {
	chatInfoLoaded: "chatInfoLoaded"
};

Events.registerEvents(Events.PagePublicChat);

/**
 * 
 * @type type
 */
AppPage.pages.public_chat_message = function()
{
	var _this = this;
	
	this.messageId = UrlParser.getParam("mId");
	
	this.run = function() 
	{
		ApiClient.Main.getPublicChatMessage(_this.messageId, function(res) 
		{
			APIResponse.process(res, function(res) 
			{
				DocumentTitle.setTitle(MetaData.formatTitleForPage("info/public_chat_message", {
					NAME: res.chat.name
				}));
				
				EventsManager.fire(Events.PagePublicChatMessage.messageInfoLoaded, res);
			},
			function(res) 
			{
				EventsManager.fire(Events.PageDirector.pageError, {
					message: res.ERRORS
				});
			});
		});
	};
};

Events.PagePublicChatMessage = {
	messageInfoLoaded: "messageInfoLoaded"
};

Events.registerEvents(Events.PagePublicChatMessage);

/**
 * 
 * @type type
 */
AppPage.pages.settings = function()
{
	this.accountPage = null;
	this.run = function() 
	{
		Session.whenUserLoaded(function(user) 
		{
			EventsManager.fire(Events.SettingsPage.pageLoaded, {
				user: user
			});
		});
	};
};

Events.SettingsPage = {
	pageLoaded: "pageLoaded"
};


Events.registerEvents(Events.SettingsPage);

/**
 * 
 * @type type
 */
AppPage.pages.account_info_update = function()
{
	this.run = function() 
	{
		Register.updateAccountInfo = function(accountData) 
		{
			ApiClient.User.updateAccountInfo(accountData, function(res) 
			{
				APIResponse.process(res, function(res) 
				{
					EventsManager.fire(Events.PageAccountInfoUpdate.infoUpdated);
					
					Session.saveUser(res.user);
					
					EventsManager.fire(Events.PageDirector.pageSuccess, {
						backButton: true
					});
					
				}, function(res) 
				{
					PageDirector.showOperationError(res.ERRORS);
				});
			});
		};
	};
};

Events.PageAccountInfoUpdate = {
	infoUpdated: "infoUpdated",
};

Events.registerEvents(Events.PageAccountInfoUpdate);

/**
 * 
 * @type type
 */
AppPage.pages.delete_account = function()
{
	this.run = function() 
	{
		Register.deleteAccount = function() 
		{
			ApiClient.User.deleteAccount(function(res) 
			{
				APIResponse.process(res, function(res) 
				{
					EventsManager.fire(Events.PageDeleteAccount.accountDeleted);
					
					NavigationManager.goToUrl(new NavigationManagerUrl().createFromRelative("?page=logout", "/"));
					
				}, function(res) 
				{
					PageDirector.showOperationError(res.ERRORS);
				});
			});
		};
	};
};

Events.PageDeleteAccount = {
	accountDeleted: "accountDeleted",
};

Events.registerEvents(Events.PageDeleteAccount);

/**
 * 
 * @type type
 */
AppPage.pages.index = function()
{
	this.run = function()
	{
		EventsManager.fire(Events.IndexPage.loaded, {
			tagName: UrlParser.getParam("tag", "")
		});
	};
};

Events.IndexPage = {
	loaded: "loaded"
};

Events.registerEvents(Events.IndexPage);

/**
 * 
 * @type type
 */
AppPage.pages.user = function()
{
	this.run = function() 
	{
		Register.userPage = new function() 
		{
			var _this = this;
			
			this.userInfo = null;
			this.userChats = null;
			this.userAccounts = null;
			
			this.loadUserInfo = function() 
			{
				if(UrlParser.getParam("userId") !== undefined) 
				{
					ApiClient.Main.getUser(UrlParser.getParam("userId"), _this.processUserRes);
				}
				else if(UrlParser.getParam("userName") !== undefined) 
				{
					ApiClient.Main.getUserByName(UrlParser.getParam("userName"), _this.processUserRes);
				}
				else
				{
					ApiClient.Main.getRandomUser(_this.processUserRes);
				}
			};
			
			this.processUserRes = function(res) 
			{
				APIResponse.process(res, function(res) 
				{
					_this.userInfo = res.user;
          
          NavigationManager.replaceUrl(new NavigationManagerUrl().createFromRelative(
							  "?page=user&userId="+res.user.userId, 
							  "/uid/"+res.user.userId));
					
					EventsManager.fire(Events.UserPage.userInfoLoaded, {
						user: res.user
					});

					var metaTemplate = "account/name";

					DocumentTitle.setTitle(MetaData.formatTitleForPage(metaTemplate, {
						TAGS: res.user.tags.tagsStringHashed
					}));
				},
				function(res) 
				{
					EventsManager.fire(Events.PageDirector.pageError, {
						message: res.ERRORS
					});
				});
			};
			
			this.loadKarmaHistory = function() 
			{
				ApiClient.Main.getUserKarmaHistory(_this.userInfo.userId, function(res) 
				{
					APIResponse.process(res, function(res) 
					{
						EventsManager.fire(Events.UserPage.userKarmaHistoryLoaded, {
							karmaHistory: res
						});
					},
					function(res) 
					{
						// TODO: add error report
					});
				});
			};
			
			this.loadChats = function() 
			{
				// TODO: temporary
				if(_this.userChats != null) 
					return;
				
				ApiClient.Main.getUserChats(_this.userInfo.userId, function(res) 
				{
					APIResponse.process(res, function(res) 
					{
						_this.userChats = res;
						
						EventsManager.fire(Events.UserPage.userChatsLoaded, {
							chats: res
						});
					},
					function(res) 
					{
						EventsManager.fire(Events.UserPage.userChatsFailedLoad, {
							message: res.ERRORS
						});
					});
				});
			};

      this.loadActiveAccounts = function()
      {
        // TODO: temporary
        if(_this.userAccounts != null)
          return;

        ApiClient.Main.getUserActiveAccounts(_this.userInfo.userId, function(res)
        {
          APIResponse.process(res, function(res)
            {
              _this.userAccounts = res;

              EventsManager.fire(Events.UserPage.userAccountsLoaded, {
                accounts: res
              });
            },
            function(res)
            {
              EventsManager.fire(Events.UserPage.userAccountsFailedToLoad, {
                message: res.ERRORS
              });
            });
        });
      };
		};
		
		Register.userPage.loadUserInfo();
	};
};

Events.UserPage = 
{
	userInfoLoaded: "userInfoLoaded",
	userKarmaHistoryLoaded: "userKarmaHistoryLoaded",
	userChatsLoaded: "userChatsLoaded",
	userChatsFailedLoad: "userChatsFailedLoad",
	userAccountsLoaded: "userAccountsLoaded",
	userAccountsFailedToLoad: "userAccountsFailedToLoad"
};

Events.registerEvents(Events.UserPage);

/**
 * 
 * @type type
 */
AppPage.pages.users_search = function()
{
	this.run = function() 
	{
		Register.usersSearch = new UsersSearch();
		Register.usersSearch.init();
	};
};

/**
 *
 * @type type
 */
AppPage.pages.chats_feed = function()
{
  this.run = function()
  {
    if(Register.chatsFeed) {
      Register.chatsFeed.refresh();
    }
  };
};

/**
 * 
 * @type type
 */
AppPage.pages.service_agreement_updated = function()
{
	this.run = function()
	{
		Register.acceptServiceAgreement = function() 
		{
			ApiClient.User.acceptServiceAgreement(function(res) 
			{
				APIResponse.process(res, function(res) 
				{
					Session.setUser(res.user);
					NavigationManager.goToUrl(new NavigationManagerUrl().createFromRelative("?page=index", "/"));
					
				}, function(res) 
				{
					PageDirector.showOperationError(res.ERRORS);
				});
			});
		};
	};
};

var UserInfo = function () 
{
	var _this = this;
	
	this.tagsManager = new UserTagsManager();
	this.filtersManager = new UserFilterManager();
	this.accountManager = new UserAccountManager();
	
	this.init = function()
	{
		this.tagsManager.init();
		this.filtersManager.init();
		this.accountManager.init();
		
		EventsManager.subscribe(Events.UpdatesWatcherData.userNotifications, function(res) 
		{
			Register.userNotifications.set(
					  res.chatInvitesAmount, 
					  res.newMessagesStats.messagesPerChat);
		});
	};
	
	
	this.switchAcceptInvitations = function() 
	{
		ApiClient.User.switchIsOpenForChat(function(res) 
		{
			APIResponse.process(res, function(res) 
			{
				EventsManager.fire(Events.Session.updateUser, {
					user: res.user
				});
			}, function(res) {
				PageDirector.showOperationError(res.ERRORS);
			});
		});
	};
	
	this.setLangs = function(langs) 
	{
		if(langs && langs.length > 0) 
		{
			ApiClient.User.setUserLangs(langs, function(res) 
			{	
				APIResponse.process(res, function(res) 
				{
					EventsManager.fire(Events.Session.updateUser, {
						user: res.user
					});
					EventsManager.fire(Events.UserInfo.userLangsChanged);
				},
				function(res) 
				{
					PageDirector.showOperationError(res.ERRORS);
				})
			});
		}
		else 
		{
			PageDirector.showOperationError(uiText(UITexts.UserInfo.error_noLangsSelected));
		}
	};
	
	this.loadKarmaHistory = function() 
	{
		ApiClient.Main.getUserKarmaHistory(Session.user.userId, function(res) 
		{
			APIResponse.process(res, function(res) 
			{
				EventsManager.fire(Events.UserInfo.userKarmaHistoryLoaded, {
					karmaHistory: res
				});
			},
			function(res) 
			{
				// TODO: add error report
			});
		});
	};
};

Events.UserInfo = 
{
	userInfoChanged: "userInfoChanged",
	userTagsChanged: "userTagsChanged",
	userFilterChanged: "userFilterChanged",
	usersListOrderChanged: "usersListOrderChanged",
	
	userFinishedEditingTags: "userFinishedEditingTags",
	userFinishedEditingFilters: "userFinishedEditingFilters",
	userRefreshedUsersList: "userRefreshedUsersList",
	
	userKarmaHistoryLoaded: "userKarmaHistoryLoaded",
	
	userLangsChanged: "userLangsChanged",
	uiLangChanged: "uiLangChanged",
	fontChanged: "fontChanged",
	themeChanged: "themeChanged"
};

Events.registerEvents(Events.UserInfo);

UITexts.UserInfo = 
{
	error_noLangsSelected: "error_noLangsSelected",
	error_maxTagsReached: "error_maxTagsReached"
};

EventsManager.subscribe(Events.Application.appStarting, function() 
{
	Register.userInfo = new UserInfo();
	Register.userInfo.init();
});



var UserTagsManager = function() 
{
	var _this = this;
	
	this.tags = [];
	
	this.init = function() 
	{
		EventsManager.subscribe(Events.UserTags.tagsChanged, function(data) {
			EventsManager.fire(Events.UserInfo.userTagsChanged, data);
		});
		
		EventsManager.subscribe(Events.Session.userLoaded, function(data) 
		{
			_this.tags = data.user.tags.tags;
			EventsManager.fire(Events.UserTags.tagsChanged, {
				tags: _this.tags
			});
		});
		
		EventsManager.subscribe(Events.UserInfo.userTagsChanged, function(data, event) 
		{
			EventsManager.subscribeOnce(Events.UserInfo.userFinishedEditingTags, function() 
			{
				var ids = _this.tags.map(function(tag) {
					return tag.id;
				});
				
				ApiClient.User.setUserTagsSettings(ids, function() {

				});
				
			}, { id: "saveTagsSettings" });
		});
	};
	
	this.create = function(tagTitle, kind, lang) 
	{
		if(this.checkTags(tagTitle, kind))
		{
			ApiClient.User.addNewTag(tagTitle, kind, lang, function(res) 
			{
				APIResponse.process(res, function(res) 
				{
					EventsManager.fire(Events.UserTags.newTagAdded, {
						tag: tagTitle,
						kind: kind,
						lang: lang
					});

					EventsManager.fire(Events.UserTags.tagAdded, {
						tagId: null,
						tagTitle: tagTitle
					});
					
					EventsManager.fire(Events.UserTags.tagsChanged, {
						tags: _this.tags
					});
					
					EventsManager.fire(Events.Session.updateUser, {
						user: res.user
					});
				},
				function(res) 
				{
					PageDirector.showOperationError(res.ERRORS);
				});
			});
		};
	};


	this.add = function(tagId, tagTitle, kind) 
	{
		if(_this.checkTags(tagTitle, kind))
		{
			_this.tags.push({
				id: tagId,
				tag: tagTitle,
				kind: kind
			});

			EventsManager.fire(Events.UserTags.tagsChanged, {
				tags: _this.tags
			});

			EventsManager.fire(Events.UserTags.tagAdded, {
				tagId: tagId,
				tagTitle: tagTitle,
				kind: kind
			});
		}
	};

	this.remove = function(tagId) 
	{
		removeFromArrayOnCondition(_this.tags, function(tag) {
			return tag.id == tagId;
		});
		
		EventsManager.fire(Events.UserTags.tagsChanged, {
			tags: _this.tags
		});
	};
	
	this.checkTags = function(tagTitle, kind) 
	{
		if(arrayCondition(this.tags, function(tag, i) {
			if(tag.tag == tagTitle && tag.kind == kind) 
			{
				return true;
			}
		})) {
			return false;
		}
		
		if(false === this.tags.length < 5) 
		{
			PageDirector.showOperationError(uiText(UITexts.UserInfo.error_maxTagsReached));
			return false;
		}
		
		return true;
	};
};

Events.UserTags = 
{
	newTagAdded: "newTagAdded",
	tagAdded: "tagAdded",
	tagsChanged: "tagsChanged",
};

Events.registerEvents(Events.UserTags);

var UserFilterManager = function() 
{
	var _this = this;
	
	this.settings = {
		options: {
			hidePrevChattedWith: false
		},
		filters: [],
		unwanted: []
	};
	
	this.init = function() 
	{
		EventsManager.subscribe(Events.UpdatesWatcherData.user, function(res) 
		{
			_this.setSettings(res.user.filterSettings.settings);
			
			EventsManager.fire(Events.UserFilterSettings.settingsLoaded, _this.settings);
		});

		
		EventsManager.subscribeMulti([Events.UserInfo.userRefreshedUsersList, Events.UserInfo.userFinishedEditingFilters], function() 
		{
			EventsManager.subscribeOnce(Events.PageDirector.pageUnloaded, function() 
			{
				if(Session.hasUser()) 
				{
					ApiClient.User.setUserFilterSettings(_this.settings, function() {
						
					});	
				}
			}, {
				id: "synchFilterSettings"
			});
		});
	};
	
	this.setSettings = function(settings) 
	{
		this.settings = settings;
    this.settings.options = {
      hidePrevChattedWith: false
    };
	};

	this.options = {
		setHidePrevChattedWith: function(val)
		{
			_this.settings.options.hidePrevChattedWith = val;
      EventsManager.fire(Events.UserFilterSettings.settingsChanged, _this.settings);
		}
	};
	
	this.filters = 
	{
		add: function(tagName) 
		{
			if(_this.settings.filters.indexOf(tagName) === -1) 
			{
				_this.settings.filters.push(tagName);
				
				EventsManager.fire(Events.UserFilterSettings.filterAdded, {
					tagTitle: tagName
				});
				EventsManager.fire(Events.UserFilterSettings.filtersChanged, _this.settings);
				EventsManager.fire(Events.UserFilterSettings.settingsChanged, _this.settings);
			}
		},

		remove: function(tagName) 
		{
			removeFromArray(_this.settings.filters, tagName);
			EventsManager.fire(Events.UserFilterSettings.filtersChanged, _this.settings);
			EventsManager.fire(Events.UserFilterSettings.settingsChanged, _this.settings);
		}
	};
	
	this.unwanted = 
	{
		add: function(tagName) 
		{
			if(_this.settings.unwanted.indexOf(tagName) === -1) 
			{
				_this.settings.unwanted.push(tagName);
				
				EventsManager.fire(Events.UserFilterSettings.unwantedTagAdded, {
					tagTitle: tagName
				});
				
				EventsManager.fire(Events.UserFilterSettings.unwantedTagsChanged, _this.settings);
				EventsManager.fire(Events.UserFilterSettings.settingsChanged, _this.settings);
			}
		},

		remove: function(tagName) 
		{
			removeFromArray(_this.settings.unwanted, tagName);
			EventsManager.fire(Events.UserFilterSettings.unwantedTagsChanged, _this.settings);
			EventsManager.fire(Events.UserFilterSettings.settingsChanged, _this.settings);
		}
	};
	
	this.reset = function() 
	{
		_this.setSettings({
			filters: [],
			unwanted: []
		});
		
		EventsManager.fire(Events.UserFilterSettings.filtersChanged, _this.settings);
		EventsManager.fire(Events.UserFilterSettings.unwantedTagsChanged, _this.settings);
		EventsManager.fire(Events.UserFilterSettings.settingsChanged, _this.settings);
	};
};

Events.UserFilterSettings = 
{
	settingsLoaded: "settingsLoaded",
	settingsChanged: "settingsChanged",
	
	filterAdded: "filterAdded",
	filtersChanged: "filtersChanged",
	
	unwantedTagAdded: "unwantedTagAdded",
	unwantedTagsChanged: "unwantedTagsChanged",
};

Events.registerEvents(Events.UserFilterSettings);


var UserAccountManager = function() 
{
	var _this = this;
	
	
	this.init = function() 
	{
		
	};
	
	this.saveAccountAbout = function(formData) 
	{
		ApiClient.User.setAccountAbout(formData, this._processAccountDataChangeResponse);
	};
	
	this.saveChattingsSettings = function(formData) 
	{
		ApiClient.User.setAccountChattingSettings(formData, this._processAccountDataChangeResponse);
	};
	
	this.saveEmailSettings = function (formData) 
	{
		ApiClient.User.setAccountEmailSettings(formData, this._processAccountDataChangeResponse);
	};
	
	this.saveNotificationsSettings = function(formData) 
	{
		ApiClient.User.setAccountNotificationSettings(formData, this._processAccountDataChangeResponse);
	};
	
	this.setFontSize = function(selector) 
	{
		Register.fontsManager.changeFontSize(UIManager.getSelectedOption(selector));
	};
	
	this._processAccountDataChangeResponse = function(res) 
	{
		APIResponse.process(res, function(res) 
		{
			PageDirector.showOperationSuccess(uiText(UITexts.Generic.saved));
			EventsManager.fire(Events.Session.updateUser, {
				user: res.user
			});
		}, function(res) 
		{
			PageDirector.showOperationError(res.ERRORS);
			Register.errorHandler.handle(null, uiText(UITexts.Generic.operationFailed));
		});
	}
};


var OnlineUsersDataProvider = function() 
{
	var _this = this;
	
	this.newDataUpdatesAmount = 0;
	
	var lastLoadedOnlineUsersData = {
		usersList: [],
		tagsList: []
	};
	
	var checksumsByLang = {};
	
	var lastFilteredOnlineUsersData = 
	{
		usersList: [],
		tagsList: []
	};
	
	var usersSortOrder = Remember.that(Remember.my.onlineUsersOrder, UsersOrder.default);
	
	this.init = function() 
	{
		EventsManager.subscribe(Events.UpdatesWatcherData.main, function() 
		{
			_this.loadOnlineUsers();
		}, {
			once: true
		});
		
		EventsManager.subscribe(Events.OnlineUsersData.dataLoaded, function() 
		{
			EventsManager.subscribeMulti([
				Events.UserFilterSettings.settingsChanged,
				Events.UserInfo.usersListOrderChanged
			],
			function(data, event) 
			{
				_this.filterUsersData(event);
			});
		}, {
			once: true
		});
		
		EventsManager.subscribe(Events.UpdatesWatcherData.onlineUsers, function(res) 
		{
			performOnEveryKey(res.langs, function(key, val) 
			{
				if(checksumsByLang[key] && checksumsByLang[key] != val) 
				{
					_this.setNewDataUpdatedAmount(++_this.newDataUpdatesAmount);
				}
				
				checksumsByLang[key] = val;
			});
		});
	};
	
	this.setOnlineUsersData = function(res) 
	{
		lastLoadedOnlineUsersData.usersList = [];
		lastLoadedOnlineUsersData.tagsList = [];
		
		performOnEveryKey(res.langs, function(lang, usersList) {
			lastLoadedOnlineUsersData.usersList = lastLoadedOnlineUsersData.usersList.concat(usersList.users.users);
			lastLoadedOnlineUsersData.tagsList = lastLoadedOnlineUsersData.tagsList.concat(usersList.tags.list);
		});
		
		EventsManager.fire(Events.OnlineUsersData.dataLoaded, lastLoadedOnlineUsersData);
	};	
	
	this.setNewDataUpdatedAmount = function(amount) 
	{
		this.newDataUpdatesAmount = amount;

		EventsManager.fire(Events.OnlineUsersData.newDataUpdatesAvaiable, {usersListPendingChangesAmount: this.newDataUpdatesAmount});
	};
	
	this.loadOnlineUsers = function() 
	{
		EventsManager.fire(Events.OnlineUsersData.dataLoading);
		
		ApiClient.Main.getOnlineUsers(function(res) 
		{
			_this.setOnlineUsersData(res);
			_this.filterUsersData();
			
			_this.setNewDataUpdatedAmount(0);
		});
	};
	
	this.filterUsersData = function() 
	{
		EventsManager.fire(Events.OnlineUsersData.dataFilteringStarted);
		
		Threader.putInQueue(function() 
		{
			lastFilteredOnlineUsersData.usersList = _this.usersFilter.filter(lastLoadedOnlineUsersData.usersList, usersSortOrder);
			lastFilteredOnlineUsersData.tagsList = _this.tagsFilter.filter(lastLoadedOnlineUsersData.tagsList, lastFilteredOnlineUsersData.usersList);
		},
		function() 
		{
			EventsManager.fire(Events.OnlineUsersData.dataFiltered, lastFilteredOnlineUsersData);
			EventsManager.fire(Events.OnlineUsersData.usersMatchingAmountChanged, {
				amount: lastFilteredOnlineUsersData.usersList.length
			});
		});
	};
	
	this.setUsersSortingOrder = function(order) 
	{
		usersSortOrder = order;

		Remember.please(Remember.my.onlineUsersOrder);

		EventsManager.fire(Events.UserInfo.usersListOrderChanged, {
			order: order
		});
	};

	this.getUsersSortingOrder = function() 
	{
		return usersSortOrder;
	};
	
	this.usersFilter = 
	{
		filter: function(users, order) // _filterUsres
		{
			var filteredUsers = [];

			performOnElsList(users, function(user)
			{
				var match = true;

        if(Register.userInfo.filtersManager.settings.options.hidePrevChattedWith)
        {
          if(Session.user.currentStrangers.indexOf(user.userId) !== -1)
          {
            match = false;
          }
				}

				performOnElsList(Register.userInfo.filtersManager.settings.filters, function(tag) {
					if(user.tags.titles.indexOf(tag) === -1) 
					{
						match = false;
					}
				});

				performOnElsList(Register.userInfo.filtersManager.settings.unwanted, function(tag) {
					if(user.tags.titles.indexOf(tag) !== -1) 
					{
						match = false;
					}
				});

				if(match) 
				{
					filteredUsers.push(user);
				}
			});

			return this.sortUsers(filteredUsers, order);
		},

		sortUsers: function(usersArray, order) 
		{
			var sortedUsers = [];

			switch(order) 
			{
				case UsersOrder.topFirst:
					sortedUsers = this.sortUsersByKarma(usersArray, true);
					break;

				case UsersOrder.newFirst:
					sortedUsers = this.sortUsersByAge(usersArray);
					break;

				case UsersOrder.default:
				default:
				{
					var topUsers = usersArray.filter(function(item) {
						return item.isPinned == true;
					});

					sortedUsers = topUsers.concat(usersArray.filter(function(item) {
						return item.isPinned == false;
					}).shuffle());
				}
				break;
			}

			return sortedUsers;
		},

		sortUsersByAge: function(users, desc) 
		{
			users.sort(function(one, another) {
				var res = DateTimeManager.dateUtcFromString(one.createdAtUtc).getTime() < DateTimeManager.dateUtcFromString(another.createdAtUtc).getTime();
				if(desc) {
					res = !res;
				}

				return res ? 1 : -1;
			});

			return users;
		},

		sortUsersByKarma: function(users, desc) 
		{
			users.sort(function(one, another) {
				var res = parseInt(one.karma) > parseInt(another.karma);
				if(desc) {
					res = !res;
				}

				return res ? 1 : -1;
			});

			return users;
		},
	};
	
	this.tagsFilter = 
	{
		filter: function(tags, users) 
		{
			var filteredTags = [];

			performOnElsList(tags, function(tagInfo) {
				var actualAmount = 0;
				performOnElsList(users, function(user) 
				{
					if(user.tags.ids.indexOf(tagInfo.id) !== -1) 
					{
						actualAmount++;
					}
				});

				if(actualAmount > 0) 
				{
					tagInfo.usersAmount = actualAmount;
					filteredTags.push(tagInfo);
				}
			});

			return filteredTags.sort(firstBy("usersAmount", -1).thenBy("tag", 1));
		}
	}
};

var UsersOrder = 
{
	default: "default",
	topFirst: "topFirst",
	newFirst: "newFirst"
};

Events.OnlineUsersData = 
{
	dataLoading: "dataLoading",
	dataLoaded: "dataLoaded",
	newDataUpdatesAvaiable: "newDataUpdatesAvaiable",
	
	dataFilteringStarted: "dataFilteringStarted",
	dataFiltered: "dataFiltered",
	usersMatchingAmountChanged: "usersMatchingAmountChanged",
	usersListChanged: "usersListChanged",
	onlineTagsListChanged: "onlineTagsListChanged"
};

Events.registerEvents(Events.OnlineUsersData);


EventsManager.subscribe(Events.Application.appStarting, function() 
{
	Register.onlineUsersDataProvider = new OnlineUsersDataProvider();
	Register.onlineUsersDataProvider.init();
});

var Main = function () 
{
	var currentMainStats = {
		online: -1,
	};
	
	this.init = function(chatId) 
	{
		
	};
	
	this.acceptServiceAgreenment = function() 
	{
		// TODO: add scroll through js-action
		ApiClient.Main.acceptServiceAgreement(function(res) 
		{
			APIResponse.process(res, 
			function(res) 
			{
				EventsManager.fire(Events.Main.serviceAgreenmentAccepted);
				
				UpdatesWatcher.pause(function() 
				{
					Session.setAuth(res.AUTH);
				});
			}, 
			function(res) {
				alert(res.ERRORS);
			});
		});
	};
};

Events.Main = 
{
	serviceAgreenmentAccepted: "serviceAgreenmentAccepted"
};

Events.registerEvents(Events.Main);


EventsManager.subscribe(Events.Application.appStarting, function() 
{
	Register.main = new Main();
	Register.main.init();
});

var UserNotifications = function()
{
	var _this = this;
	
	this.data = {
		invitesAmount: 0,
		messages: []
	};
	
	this.init = function() 
	{
		var prevTotal = 0;
		
		EventsManager.subscribe(Events.UserNotifications.updated, function(data) 
		{
			if(data.total != prevTotal) {
				EventsManager.fire(Events.UserNotifications.newNotifications, _this.getTotal());
			}
			
			prevTotal = data.total;
		});
	};
	
	this.set = function(invitesAmount, messages) 
	{
		this.data.invitesAmount = parseInt(invitesAmount);
		this.data.messages = messages;
		
		EventsManager.fire(Events.UserNotifications.updated, this.getTotal());
	};
	
	this.getTotal = function() 
	{
		var totals = 
		{
			messages: 0,
			invites: this.data.invitesAmount,
			total: 0
		};
		
		performOnEveryKey(this.data.messages, function(chatId, msgsAmount) {
			totals.messages += parseInt(msgsAmount);
		});
		
		totals.total = totals.messages + totals.invites;
		
		return totals;
	};
	
	this.dismissMessages = function(chatId) 
	{
		this.data.messages[chatId] = 0;
		
		EventsManager.fire(Events.UserNotifications.updated, this.getTotal());
	};
};

Events.UserNotifications = 
{
	updated: "updated",
	newNotifications: "newNotifications"
};

Events.registerEvents(Events.UserNotifications);

EventsManager.subscribe(Events.Application.appReady, function() 
{
	Register.userNotifications = new UserNotifications();
	Register.userNotifications.init();
});

var UILangManager = function () 
{
	this.setUILang = function(lang) 
	{
		ApiClient.Main.setUILang(lang, function() 
		{
			EventsManager.fire(Events.UserInfo.uiLangChanged, lang);
		});
	};
	
};

EventsManager.subscribe(Events.Application.appStarting, function() 
{
	Register.uiLangManager = new UILangManager();
});

var UserChats = function() 
{
	var _this = this;
	
	this.newMessagesPerChat = [];
	this.selectedChatId = null;
	
	this.init = function() 
	{
		EventsManager.subscribeMulti([Events.Chat.chatSelected, Events.Chat.chatReset], function(data, event) 
		{
			_this.selectedChatId = event == Events.Chat.chatSelected ? data.chatId : null;
			
			EventsManager.fire(Events.UserChats.selectedChatChanged, {
				chatId: _this.selectedChatId
			});
		});
		
		EventsManager.subscribe(Events.UpdatesWatcherData.userChats, function(res) 
		{
			var activeChats = [];
			var closedChats = [];

			performOnElsList(res.chats, function(chat) {
				if(!chat.isClosed) {
					activeChats.push(chat);
				} else {
					closedChats.push(chat);
				}
			});

			EventsManager.fire(Events.UserChats.activeChatsUpdated, {
				data: {
					chats: activeChats,
					totalAmount: activeChats.length
				}
			});

			EventsManager.fire(Events.UserChats.closedChatsUpdated, {
				data: {
					chats: closedChats,
					totalAmount: closedChats.length
				}
			});
		});
		
		EventsManager.subscribe(Events.UpdatesWatcherData.userNotifications, function(res) 
		{
			_this.newMessagesPerChat = res.newMessagesStats.messagesPerChat;
			
			EventsManager.fire(Events.UserChats.newMessagesStatsUpdated, {
				data: res.newMessagesStats
			});
		});
		
		EventsManager.subscribe(Events.UpdatesWatcherData.userChatInvites, function(res) 
		{
			EventsManager.fire(Events.UserChats.invitesUpdated, {
				data: res
			});
		});
	};
	
	this.getNewMessagesAmountForChat = function(chatId) 
	{
		return OR(_this.newMessagesPerChat[chatId], 0);
	};
	
	this.invite = function(userId, data) 
	{
		ApiClient.Chat.inviteUserToChat(userId, data, function(res) 
		{
			APIResponse.process(res, function(res) 
			{
				NavigationManager.goToUrl(new NavigationManagerUrl().createFromRelative(
						  "?page=chat&chatId="+res.chat.chatId, 
						  "/cid/"+res.chat.chatId));
			},
			function(res) {
				PageDirector.showOperationError(res.ERRORS);
			});
		});
	};
	
	this.acceptInvitation = function(chatId) 
	{
		ApiClient.Chat.acceptInvitation(chatId, function(res) 
		{
			APIResponse.process(res, function(res) 
			{
				EventsManager.fire(Events.UserChats.invitationAccepted, {
					chatId: chatId
				}, {
					subtype: chatId
				});
				
			}, function(res) {
				PageDirector.showOperationError(res.ERRORS);
			});
		});
	};
	
	this.rejectInvitation = function(chatId) 
	{
		ApiClient.Chat.rejectInvitation(chatId, function(res) 
		{
			APIResponse.process(res, function(res) 
			{
				EventsManager.fire(Events.UserChats.invitationRejected, {
					chatId: chatId
				}, {
					subtype: chatId
				});

			}, function(res) {
				PageDirector.showOperationError(res.ERRORS);
			});
		});
	};
	
	this.finishChat = function(chatId) 
	{
		ApiClient.Chat.disconnect(chatId, function()
		{	

		});
	};
	
	this.complain = function(chatId, formData) 
	{
		ApiClient.Chat.complainOnChat(chatId, formData, function(res) 
		{
			APIResponse.process(res, function() 
			{
				EventsManager.fire(Events.UserChats.complainSubmitted);
			}, 
			function(res) 
			{
				PageDirector.showOperationError(res.ERRORS);
			});
		});
	};
};

Events.UserChats = 
{
	selectedChatChanged: "selectedChatChanged",
	invitesListRendered: "invitesListRendered",
	activeListRendered: "activeListRendered",
	
	invitesUpdated: "invitesUpdated",
	activeChatsUpdated: "activeChatsUpdated",
	closedChatsUpdated: "closedChatsUpdated",
	newMessagesStatsUpdated: "newMessagesStatsUpdated",
	
	invitationAccepted: "invitationAccepted",
	invitationRejected: "invitationRejected",
	complainSubmitted: "complainSubmitted",
};

Events.registerEvents(Events.UserChats);

EventsManager.subscribe(Events.Application.appStarting, function() 
{
	Register.userChats = new UserChats();
	Register.userChats.init();
});

var OnlineUsers = function() 
{
	var _this = this;
	
	this.currentUsersList = null;
	
	this.startingFrom = 0;
	this.usersPerPage = 10;
	
	
	this.init = function() 
	{
		EventsManager.subscribe(Events.OnlineUsersData.dataFiltered, function(res, event, subtype) 
		{
			_this.currentUsersList = res.usersList;
			_this.refreshUsersList();
		});
	};
	
	
	this.refreshUsersList = function() 
	{
		this.startingFrom = 0;
		
		this.addUsersToShow();
		
		EventsManager.fire(Events.UserInfo.userRefreshedUsersList);
	};
	
	this.showMore = function() 
	{
		this.startingFrom += this.usersPerPage;
		this.addUsersToShow();
	};
	
	this.addUsersToShow = function() 
	{
		EventsManager.fire(Events.OnlineUsersList.usersAdded, {
			users: _this.currentUsersList.slice(_this.startingFrom, _this.startingFrom + _this.usersPerPage),
			totalAmount: _this.currentUsersList.length,
			currentAmount: _this.startingFrom + _this.usersPerPage
		});
	};
};

Events.OnlineUsersList = 
{
	usersAdded: "usersAdded"
};

Events.registerEvents(Events.OnlineUsersList);


EventsManager.subscribe(Events.Application.appStarting, function() 
{
	Register.onlineUsers = new OnlineUsers();
	Register.onlineUsers.init();
});

var PopularTags = function() 
{
	var _this = this;
	
	this.tagsData = {
		startingFrom: 0,
		amount: 500,
		
		loadedTags: [],
		totalAmount: -1,
		
		newTags: [],
		leftAmount: 0
	};
	
	this.init = function() 
	{
		EventsManager.subscribe(Events.PopularTags.loadRequested, function() 
		{
		  _this.loadTags();

		  EventsManager.subscribeOnce(Events.PopularTags.loaded, function()
      {
        _this.search("");
      });
		}, {
			once: true
		});
		
		EventsManager.subscribe(Events.UserInfo.userLangsChanged, function(langs) 
		{
			_this.reset();
		});

		EventsManager.subscribe(Events.PopularTags.searchRequested, function(input)
    {
		  _this.search(input);
    });
	};
	
	this.loadTags = function() 
	{
		EventsManager.fire(Events.PopularTags.loading);
		
		ApiClient.Main.getPopularTags(_this.tagsData.startingFrom, _this.tagsData.amount, function(res) 
		{
			_this.tagsData.loadedTags = _this.tagsData.loadedTags.concat(res.list);
			_this.tagsData.totalAmount = res.totalAmount;
			_this.tagsData.newTags = res.list;
			_this.tagsData.leftAmount = _this.tagsData.totalAmount - _this.tagsData.loadedTags.length;

			EventsManager.fire(Events.PopularTags.loaded, _this.tagsData);
		});
	};
	
	this.loadMoreTags = function() 
	{
		_this.tagsData.startingFrom += _this.tagsData.amount;
		_this.loadTags();
	};

	this.search = function(input)
	{
    EventsManager.fire(Events.PopularTags.searchStarted);

    var result = {
    	matchingTags: _this.tagsData.loadedTags.slice(0, 20)
		};

    if(input.length > 0)
    {
      result.matchingTags = this.searchInLoaded(input).slice(0, 10);

      if(result.matchingTags.length < 10 && _this.tagsData.leftAmount > 0)
      {
        this.loadMoreTags();

        EventsManager.subscribe(Events.PopularTags.loaded, function()
        {
          _this.search(input);
        });
      }
		}

    EventsManager.fire(Events.PopularTags.searchFinished, result);
	};

	this.searchInLoaded = function(input)
	{
	  return _this.tagsData.loadedTags.filter(function(tagInfo)
		{
			return UIFormat.prepareTag(tagInfo.tag).indexOf(UIFormat.prepareTag(input)) !== -1;
		});
	};
	
	this.reset = function() 
	{
		_this.tagsData.loadedTags = [];
		_this.tagsData.startingFrom = 0;
		EventsManager.fire(Events.PopularTags.reset);
		
		_this.loadTags();
	};
};

Events.PopularTags = 
{
	loadRequested: "loadRequested",
	loading: "loading",
	loaded: "loaded",
	reset: "reset",

  searchRequested: "searchRequested",
	searchStarted: "searchStarted",
	searchFinished: "searchFinished"
};

Events.registerEvents(Events.PopularTags);

EventsManager.subscribe(Events.Application.appStarting, function() 
{
	Register.popularTags = new PopularTags();
	Register.popularTags.init();
});

/**
 * 
 * @returns {ChatHistory}
 */
var ChatHistory = function(chat, viewAsUser)
{
	var _this = this;
	
	this.amountPerPage = AppInfo.values.chat.history.messagesPerPage;
	
	this.chat = chat;
	this.viewAsUser = viewAsUser;
	this.provider = null;
	
	/**
	 * @var {NavigationManagerUrl} 
	 */
	this.pageUrl = null;
	
	this.setProvider = function(func) 
	{
		this.provider = func;
	};
	
	this.show = function() 
	{
		this.loadHistory(parseInt(UrlParser.getParam("startingFrom", 0)));
	};
	
	this.loadHistory = function(startingFrom) 
	{
		EventsManager.fire(Events.ChatHistory.historyLoading, {
			chat: _this.chat
		});

		this.provider(startingFrom, _this.amountPerPage, function(historyRes) 
		{
			var pagesAmount = Math.ceil(historyRes.totalAmount / _this.amountPerPage);

			var paginationData = 
			{
				firstPage: 0,
				prevPage: startingFrom - _this.amountPerPage,
				nextPage: startingFrom + _this.amountPerPage,
				lastPage: (pagesAmount-1) * _this.amountPerPage,
				currentPage: (startingFrom),
				pagesList: []
			};

			for(var i = 0; i < historyRes.totalAmount; i+=_this.amountPerPage)
			{
				paginationData.pagesList.push({
					startingFrom: i,
					title: Math.ceil(i/_this.amountPerPage)
				});
			}

			if(paginationData.firstPage == startingFrom) {
				paginationData.firstPage = null;
				paginationData.prevPage = null;
			}

			if((startingFrom + _this.amountPerPage) > ((pagesAmount-1) * _this.amountPerPage)) {
				paginationData.lastPage = null;
				paginationData.nextPage = null;
			}						

			EventsManager.fire(Events.ChatHistory.historyLoaded, {
				chat: _this.chat,
				history: historyRes,
				viewAsUser: _this.viewAsUser,
				pagination: paginationData,
				pageUrl: _this.pageUrl
			});
		});
	};
};

Events.ChatHistory = {
	historyLoading: "historyLoading",
	historyLoaded: "historyLoaded"
};

Events.registerEvents(Events.ChatHistory);

var UserChat = function (handler) 
{
	var _this = this;

	this.chatId = null;
	
	this.updates = 
	{
		chatInfo: null,
		chatState: null,
		karmaVote: null
	};
	
	this.minMessageId = 0;
	this.maxMessageId = 0;
	this.minMessageIdLoaded = 0;
	this.maxMessageIdLoaded = 0;
	this.lastReadMessageId = 0;
	
	
	this.prevMessageInputKey = "messageInput";
	
	this.messaging = new ChatComponents.Messaging(this, handler);
	this.feedback = new ChatComponents.Feedback(this, handler);
	this.options = new ChatComponents.Options(this, handler);
	this.typing = new ChatComponents.Typing(this, handler);
	this.utils = new ChatComponents.Utils(this, handler);
	this.api = new ChatComponents.Api(this, handler);
	
	this.chatUpdatesCurrentData = {
		chatId: null,
		strangerId: null,
		lastMessageId: null,
		lastReadMessageId: null
	};
		
	this.getUpdatesId = function() 
	{
		return "chatBase_" + this.chatId;
	};
	
	
	this.init = function() 
	{
		this.options.init();
		this.typing.init();
		this.messaging.init();
		this.utils.init();
	};
	
	this.startChatWithId = function(chatId) 
	{
		this.chatId = chatId;
		
		EventsManager.fire(Events.Chat.chatSelected, {
			chatId: this.chatId
		});
		
		this.registerUpdatesCheking();
		
		EventsManager.subscribe(Events.Chat.infoUpdated, function(data) 
		{
			DocumentTitle.setTitle(MetaData.formatTitleForPage("user_chats/id", { 
				NAME: data.chat.nameCustom 
			}));
		});
		
		EventsManager.subscribe(Events.UserNotifications.newNotifications, function() 
		{
			Register.userNotifications.dismissMessages(_this.chatId);
		}, {
			id: "dismissChatMessagesNotif"
		});
	};
	
	this.registerUpdatesCheking = function() 
	{
		UpdatesWatcher.request.setChecksumForProviders([
			UpdatesWatcher.providers.chatInfo,
			UpdatesWatcher.providers.chatState,
			UpdatesWatcher.providers.chatMessages,
			UpdatesWatcher.providers.chatStrangerInfo,
			UpdatesWatcher.providers.chatStrangerKarmaVote
		], -1);
			
		UpdatesWatcher.dataProviders.registerMulti([
			UpdatesWatcher.providers.chatState,
			UpdatesWatcher.providers.chatInfo,
			UpdatesWatcher.providers.chatMessages,
			UpdatesWatcher.providers.chatStrangerInfo,
			UpdatesWatcher.providers.chatStrangerKarmaVote,
		], function() 
		{
			_this.chatUpdatesCurrentData.chatId = _this.chatId;
			_this.chatUpdatesCurrentData.lastMessageId = _this.maxMessageIdLoaded;
			_this.chatUpdatesCurrentData.lastReadMessageId = _this.lastReadMessageId;

			return _this.chatUpdatesCurrentData;
		}, {id: _this.getUpdatesId()});
				
		EventsManager.subscribe(Events.UpdatesWatcherData.chatState, function(res)
		{
			_this.getChatStateUpdatesCallback(res);
		}, {id: _this.getUpdatesId()});

		EventsManager.subscribe(Events.UpdatesWatcherData.chatInfo, function(res)
		{			
			_this.loadChatInfoCallback(res.chat);
			
		}, {id: _this.getUpdatesId()});
		
		EventsManager.subscribe(Events.UpdatesWatcherData.chatMessages, function(res)
		{
			_this.getChatMessagesCallback(res);
		}, 
		{
			id: _this.getUpdatesId(),
			noQ: true
		});
		
		EventsManager.subscribe(Events.UpdatesWatcherData.chatStrangerInfo, function(res)
		{
			_this.chatUpdatesCurrentData.strangerId = res.user.userId;
		}, {id: _this.getUpdatesId()});
		
		EventsManager.subscribe(Events.UpdatesWatcherData.chatStrangerKarmaVote, function(res)
		{
			_this.updates.karmaVote = res.vote;
			
			EventsManager.fire(Events.Chat.karmaVoteUpdated, {
				vote: res.vote
			});
			
		}, _this.getUpdatesId());
	};
	
	this.loadChatInfoCallback = function(chat)
	{
		EventsManager.fire(Events.Chat.infoUpdated, {
			chat: chat
		});
		
		_this.updates.chatInfo = chat;
	};
	

	this.getChatMessagesCallback = function (res) 
	{
		_this.processMessages(res.messages);
		
		var messages = [].concat(res.messages).reverse();
		
		EventsManager.fire(Events.Chat.newMessages, {
			messages: messages,
			isHistory: false
		}, {
			subtype: _this.chatId
		});
		
		EventsManager.fire(Events.Chat.messagesLoaded, null, {
			subtype: _this.chatId
		});
	};
	
	this.processMessages = function(messagesList) 
	{
		Session.whenUserLoaded(function() 
		{
			performOnElsList(messagesList, function(msg) 
			{
				_this.minMessageIdLoaded = Math.min((_this.minMessageIdLoaded == 0 ? msg.numericId : _this.minMessageIdLoaded), msg.numericId);
				_this.maxMessageIdLoaded = Math.max(_this.maxMessageIdLoaded, msg.numericId);

				if(msg.sentBy != Session.user.userId) 
				{
					var currentLastReadMsgId = _this.lastReadMessageId;

					_this.lastReadMessageId = Math.max(OR(_this.lastReadMessageId, msg.numericId), msg.numericId);

					if(_this.lastReadMessageId > currentLastReadMsgId) 
					{
						_this.api.updateLastReadMessage(_this.lastReadMessageId);
					}
				}
			});

			EventsManager.fire(Events.Chat.messagesLoaded, null, {
				subtype: _this.chatId
			});
		});
	};
	
	this.getChatStateUpdatesCallback = function (res) 
	{
		_this.minMessageId = res.extremes.firstMessageId;
		_this.maxMessageId = res.extremes.lastMessageId;
		
		EventsManager.fire(Events.Chat.strangerTypingStateChanged, {
			state: res.state.typingState
		});
		
		if(!_this.updates.chatState 
		|| _this.updates.chatState.state.lastReadMessageId != res.state.lastReadMessageId) 
		{
			EventsManager.fire(Events.Chat.strangerLastReadMessageChanged, {
				messageId: res.state.lastReadMessageId
			});
		}
		
		_this.updates.chatState = res;
	};
	
	this.loadMessagesHistory = function() 
	{
		ApiClient.Chat.getMessagesHistoryOlderThan(_this.chatId, this.minMessageIdLoaded, 50, function (res) 
		{
			APIResponse.process(res, function(res) 
			{
				EventsManager.fire(Events.Chat.newMessages, {
					messages: res.messages,
					isHistory: true
				}, {
					subtype: _this.chatId
				});
				
				_this.processMessages(res.messages);
			});
		});
	};
	
	this.resetChat = function() 
	{
		UpdatesWatcher.dataProviders.unregister(this.getUpdatesId());
		EventsManager.unsubscribe(Events.UserNotifications.newNotifications, "dismissChatMessagesNotif");
		
		EventsManager.unsubscribeEvery([
			Events.UpdatesWatcherData.chatInfo,
			Events.UpdatesWatcherData.chatState,
			Events.UpdatesWatcherData.chatMessages,
			Events.UpdatesWatcherData.chatStrangerInfo,
			Events.UpdatesWatcherData.chatStrangerKarmaVote
		]);
		
		EventsManager.fire(Events.Chat.chatReset, {
			chatId: this.chatId
		});

		this.chatId = null;
	};	
};

var ChatComponents = {
	
};

Events.Chat = 
{
	infoUpdated: "infoUpdated",
	chatSelected: "chatSelected",
	chatReset: "chatReset",
	
	strangerTypingStateChanged: "strangerTypingStateChanged",
	strangerLastReadMessageChanged: "strangerLastReadMessageChanged",
	
	messageInput: "messageInput",
	messageInputCanceled: "messageInputCanceled",
	
	sendingMessage: "sendingMessage",
	sendingMessageFinished: "sendingMessageFinished",
	messageSent: "messageSent",
	messageCanceled: "messageCanceled",
	sendingMessageFailed: "sendingMessageFailed",
	
	newMessages: "newMessages",
	messagesLoaded: "messagesLoaded",
	
	karmaVoteUpdated: "karmaVoteUpdated",
	feedbackSent: "feedbackSent",
};

Events.registerEvents(Events.Chat);

UITexts.UserChat = 
{
	chatStarted: "chatStarted",
	
	rejectInvitationConfirmation: "rejectInvitationConfirmation",
	
	specifyChatNamePrompt: "specifyChatNamePrompt",
	
	complainReceived: "complainReceived"
};

var ChatStatus = 
{
	NO_ACCESS: 'NO_ACCESS',
	NOT_STARTED_CONNECTED: 'NOT_STARTED_CONNECTED',
	ACCEPTED: 'ACCEPTED',
	NOT_ACCEPTED: 'NOT_ACCEPTED',
	CLOSED: 'CLOSED',
	CLOSED_AFTER_ACCEPTED: 'CLOSED_AFTER_ACCEPTED',
	REJECTED: 'REJECTED',
	
	isChatNotStarted: function(status)
	{
		return (status == this.NOT_ACCEPTED || status == 'NOT_STARTED_CONNECTED');
	},
	
	isChatStarted: function(status)
	{
		return (status == this.ACCEPTED);
	},
	
	isChatActive: function(status) 
	{
		return (status == this.NOT_STARTED_CONNECTED) || (status == this.ACCEPTED);
	},
	
	isChatClosed: function(status) 
	{
		return (status == this.CLOSED || status == this.CLOSED_AFTER_ACCEPTED);
	},
	
	isChatClosedAfterStarted: function(status) 
	{
		return (status == this.CLOSED_AFTER_ACCEPTED);
	},
	
	isChatRejected: function(status) 
	{
		return (status == this.REJECTED);
	}
};

var ChatTypingState = 
{
	VOID: "VOID",
	TYPING: "TYPING",
	STOPPED_TYPING: "STOPPED_TYPING"
}

/**
 * 
 * @param {UserChat} chat
 * @returns {UserChat.Api}
 */
ChatComponents.Api = function(chat) 
{
	/**
	 * 
	 * @returns {ChatComponents.Api.ws}|{ChatComponents.Api.http}
	 */
	this.get = function() 
	{
		if(AppInfo.getConnector() == ConnectorTypes.ws) {
			return ChatComponents.Api.ws;
		}
		
		if(AppInfo.getConnector() == ConnectorTypes.http) {
			return ChatComponents.Api.http;
		}
	};
	
	this.updateLastReadMessage = function(msgId) 
	{
		this.get().updateLastReadMessage(chat.chatId, msgId);
	};
	
	this.updateTypingState = function(state) 
	{
		this.get().updateTypingState(chat.chatId, state);
	};
};

ChatComponents.Api.ws = 
{
	updateLastReadMessage: function(chatId, msgId) 
	{
		UpdatesWatcher.watchers.subscriber.commands.perform("setLastReadMessage", {
			chatId: chatId,
			messageId: msgId
		});
	},
	updateTypingState: function(chatId, state) 
	{
		UpdatesWatcher.watchers.subscriber.commands.perform("setTypingState", {
			chatId: chatId,
			typingState: state
		});
	},
};

ChatComponents.Api.http = 
{
	updateLastReadMessage: function(chatId, msgId) 
	{
		
	},
	updateTypingState: function(chatId, state) 
	{
		ApiClient.Chat.sendTypingState(chatId, state, function (res) 
		{
			
		});
	},
};

/**
 * 
 * @param {UserChat} chat
 * @param {type} handler
 * @returns {Chat.Messaging}
 */
ChatComponents.Messaging = function(chat, handler) 
{
	var _this = this;

	
	this.init = function() 
	{
		
	};

	this.sendMessage = function (messageText) 
	{		
		if(messageText.length == 0)
			return;
		
		var tempId = new Date().getTime();
		
		EventsManager.fire(Events.Chat.sendingMessage, {
			message: messageText,
			tempId: tempId
		});
		
		ApiClient.Chat.sendMessage(chat.chatId, messageText, function (res) 
		{
			APIResponse.process(res, function(res) 
			{
				EventsManager.fire(Events.Chat.messageSent, {
					message: res.message
				}, {
					subtype: tempId
				});
				
				EventsManager.fire(Events.Chat.newMessages, {
					messages: [res.message],
					isHistory: false
				}, {
					subtype: chat.chatId
				});
			}, 
			function(res) 
			{
				EventsManager.fire(Events.Chat.sendingMessageFailed, {
					message: messageText,
					error: res.ERRORS
				}, {
					subtype: tempId
				});
			});

			EventsManager.fire(Events.Chat.sendingMessageFinished, {
				message: messageText
			}, {
				subtype: tempId
			});
		},
		function(e) 
		{
			EventsManager.fire(Events.Chat.sendingMessageFinished, {
				message: messageText
			}, {
				subtype: tempId
			});
			
			EventsManager.fire(Events.Chat.sendingMessageFailed, {
				message: messageText,
				error: ""
			}, {
				subtype: tempId
			});
		});
	};
};

/**
 * 
 * @param {UserChat} chat
 * @param {type} handler
 * @returns {UserChat.Typing}
 */
ChatComponents.Typing = function(chat, handler) 
{
	var _this = this;
	
	this.userTypingState = ChatTypingState.VOID;
	
	this.timeoutStartedTyping = null;
	this.timeoutTypingStateUpdate = null;
	
	this.init = function() 
	{
		EventsManager.subscribe(Events.Chat.sendingMessage, function() 
		{
			_this.resetUserTypingState();
		});
		
		UIManager.addEvent(window, "unload", function(e)
		{
			if(chat.chatId) {
				_this.resetUserTypingState(true);
			}
		});
	}
	
	this.setUserTyping = function() 
	{
		if(_this.userTypingState != ChatTypingState.TYPING) 
		{
			_this.userTypingState = ChatTypingState.TYPING;

			_this.timeoutStartedTyping = Timeout.set(function() 
			{
				_this.sendUserTypingState(ChatTypingState.TYPING);

				_this.scheduleUserStoppedTyping();

			}, 1000);
		} 

		if(_this.userTypingState == ChatTypingState.TYPING) 
		{
			_this.scheduleUserStoppedTyping();
		}
	};
	
	
	this.sendUserTypingState = function (state) 
	{
		chat.api.updateTypingState(state);
	};
	
	this.scheduleUserStoppedTyping = function() 
	{
		Timeout.reset(_this.timeoutTypingStateUpdate);

		_this.timeoutTypingStateUpdate = Timeout.set(function() {
			_this.setUserStopedTyping();
		}, 5000);
	};
	
	this.setUserStopedTyping = function() 
	{
		if(_this.userTypingState == ChatTypingState.TYPING) 
		{	
			_this.userTypingState = ChatTypingState.STOPPED_TYPING;
			_this.sendUserTypingState(ChatTypingState.STOPPED_TYPING);
		}
	};
	
	this.resetUserTypingState = function(send) 
	{
		this.userTypingState = ChatTypingState.VOID;
		
		if(send == true) {
			this.sendUserTypingState(ChatTypingState.VOID);
		}
		
		Timeout.reset(this.timeoutStartedTyping);
		Timeout.reset(this.timeoutTypingStateUpdate);
	};
};

/**
 * 
 * @param {UserChat} chat
 * @param {type} handler
 * @returns {UserChat.Feedback}
 */
ChatComponents.Feedback = function(chat, handler) 
{
	var _this = this;
	
	this.voteForUser = function(value) 
	{
		if(chat.updates.karmaVote.value == value) {
			value = 0;
		}
		
		ApiClient.Chat.setKarvaVote(chat.chatId, value, function(res)
		{
			APIResponse.process(res, function(res) 
			{
				chat.updates.karmaVote.value = value;
				EventsManager.fire(Events.Chat.karmaVoteUpdated, {
					vote: res.vote
				});
			},
			function(res) 
			{
				PageDirector.showOperationError(res.ERRORS);
			});
		});
	};
	
	this.sendFeedback = function(data) 
	{
		ApiClient.Chat.leaveKarvaFeedback(chat.chatId, data, function(res)
		{
			APIResponse.process(res, function() 
			{
				EventsManager.fire(Events.Chat.feedbackSent, {
					message: res.message
				});
			});
		});
	};
};

/**
 * 
 * @param {UserChat} chat
 * @param {type} handler
 * @returns {UserChat.Options}
 */
ChatComponents.Options = function(chat, handler) 
{
	var _this = this;
	
	this.init = function() 
	{
		EventsManager.fire(Events.ChatOptions.sendOnEnterChanged, {
			isSendOnEnter: _this.isSendOnEnter()
		});
	};
	
	this.isSendOnEnter = function() 
	{
		return getBool(Remember.that(Remember.my.chatSendOnEnter, true));
	};
	
	this.setSendOnEnter = function(value) 
	{
		Remember.please(Remember.my.chatSendOnEnter, value);
		
		EventsManager.fire(Events.ChatOptions.sendOnEnterChanged, {
			isSendOnEnter: _this.isSendOnEnter()
		});
	};
	
	this.setCustomName = function() 
	{
		var currentName = chat.updates.chatInfo.nameCustom;
		
		PageAlerts.prompt(uiText(UITexts.UserChat.specifyChatNamePrompt), currentName, function(newName) 
		{
			if(newName !== null && newName.trim() != currentName.trim()) 
			{
				ApiClient.Chat.setCustomName(chat.chatId, newName, function(res) 
				{
					APIResponse.process(res, null, function(res) 
					{
						PageDirector.showOperationError(res.ERRORS);
					});
				});
			}
		});
	};
	
	this.setAcceptMedia = function(accept) 
	{
		ApiClient.Chat.setAcceptMedia(chat.chatId, accept, function(res) 
		{
			APIResponse.process(res, null, function(res) 
			{
				PageDirector.showOperationError(res.ERRORS);
			});
		});
	};
	
	this.setAccess = function(access) 
	{
		ApiClient.Chat.setAccess(chat.chatId, access, function(res) 
		{
			APIResponse.process(res, function() 
			{
				EventsManager.fire(Events.ChatOptions.chatAccessChanged, {
					access: access
				});
			},
			function(res) 
			{
				PageDirector.showOperationError(res.ERRORS);
			});
		});
	};

	this.requestAccessChange = function(access)
	{
    ApiClient.Chat.requestAccessChange(chat.chatId, access, function(res)
    {
      APIResponse.process(res, function()
        {
          EventsManager.fire(Events.ChatOptions.chatAccessChangeRequested, {
            access: access
          });
        },
        function(res)
        {
          PageDirector.showOperationError(res.ERRORS);
        });
    });
	};

  this.acceptAccessChange = function()
  {
    ApiClient.Chat.acceptAccessChange(chat.chatId, function(res)
    {
      APIResponse.process(res, function()
        {
          EventsManager.fire(Events.ChatOptions.chatAccessChangeAccepted);
        },
        function(res)
        {
          PageDirector.showOperationError(res.ERRORS);
        });
    });
  };

  this.declineAccessChange = function()
  {
    ApiClient.Chat.declineAccessChange(chat.chatId, function(res)
    {
      APIResponse.process(res, function()
        {
          EventsManager.fire(Events.ChatOptions.chatAccessChangeDeclined);
        },
        function(res)
        {
          PageDirector.showOperationError(res.ERRORS);
        });
    });
  };

  this.cancelAccessChange = function()
  {
    ApiClient.Chat.cancelAccessChange(chat.chatId, function(res)
    {
      APIResponse.process(res, function()
        {
          EventsManager.fire(Events.ChatOptions.chatAccessChangeCanceled);
        },
        function(res)
        {
          PageDirector.showOperationError(res.ERRORS);
        });
    });
  };

};

Events.ChatOptions = {
	sendOnEnterChanged: "sendOnEnterChanged",
	chatAccessChanged: "chatAccessChanged",
	chatAccessChangeRequested: "chatAccessChangeRequested",
  chatAccessChangeAccepted: "chatAccessChangeAccepted",
  chatAccessChangeDeclined: "chatAccessChangeDeclined",
  chatAccessChangeCanceled: "chatAccessChangeCanceled",
};

Events.registerEvents(Events.ChatOptions);

/**
 * 
 * @param {UserChat} chat
 * @param {type} handler
 * @returns {UserChat.Typing}
 */
ChatComponents.Utils = function(chat, handler) 
{
	var _this = this;
	
	this.displayedMessageIds = [];
	
	this.init = function() 
	{
		
	}
	
	this.getStrangerLastReadMessageId = function() 
	{
		return chat.updates.chatState ? chat.updates.chatState.state.lastReadMessageId : 0;
	};
	
	this.processMessageInput = function(messageInput, e) 
	{
		var toSend = false;
		
		// 13 = enter
		if(chat.options.isSendOnEnter()) {
			toSend = e.keyCode == 13 && !e.shiftKey
		} else {
			toSend = e.keyCode == 13 && (e.ctrlKey || e.metaKey);
		}
		
		if (toSend)
		{
			chat.messaging.sendMessage(UIManager.getValueTrimmed(messageInput));
			e.preventDefault();
		}
	};
	
	var prevInputVal = "";
	
	this.processMessageTyping = function(messageInput, e) 
	{
		var currentInputVal = UIManager.getValueTrimmed(messageInput);
		
		if(currentInputVal != "") 
		{
			if(currentInputVal != prevInputVal) 
			{
				chat.typing.setUserTyping();

				EventsManager.fire(Events.Chat.messageInput, {
					message: currentInputVal
				}, {
					noQ: true
				});
			}
		}
		else 
		{
			EventsManager.fire(Events.Chat.messageInputCanceled, {
				message: UIManager.getValue(messageInput)
			}, {
				noQ: true
			});
		}
		
		prevInputVal = currentInputVal;
	};
	
	this.isMessageShouldBeAbove = function(isHistory) 
	{
		var isAbove = isHistory;
		
		if(AppInfo.isMobile()) {
			isAbove = !isAbove;
		}
		
		return isAbove;
	}
};

var UserUploadsManager = function() 
{
	var _this = this;

	this.uploadsData = {
		startingFrom: 0,
		amount: 12,
		loaded: 0
	};

	this.fileToUpload = null;

	this.processFiles = function(files) 
	{
		performOnElsList(files, function(file) {
			_this.fileToUpload = file;
		});
	};

	this.getUploads = function()
	{
    EventsManager.fire(Events.UploadsManager.getUploadsStarted);

    ApiClient.User.getUploads(_this.uploadsData.startingFrom, _this.uploadsData.amount, function(res)
    {
			EventsManager.fire(Events.UploadsManager.getUploadsFinished);

			APIResponse.process(res, function(res)
			{
				_this.uploadsData.loaded += res.uploads.length;
				EventsManager.fire(Events.UploadsManager.getUploadsLoaded, {
					res: res
				});
			},
			function(res)
			{
				PageDirector.showOperationError(res.ERRORS);
			});

    });
	};

	this.getMoreUploads = function()
	{
		_this.uploadsData.startingFrom += _this.uploadsData.amount;
		_this.getUploads();
	};

	this.resetUploadsData = function()
	{
		_this.uploadsData.startingFrom = 0;
		_this.uploadsData.loaded = 0;
	};
	
	this.uploadFile = function() 
	{
		if(_this.fileToUpload) 
		{
			if(AppInfo.values.uploadsManager.allowedTypes.indexOf(_this.fileToUpload.type) !== -1 
			&& _this.fileToUpload.size <= AppInfo.values.uploadsManager.maxSizeBytes) 
			{
				EventsManager.fire(Events.UploadsManager.uploadStarted);

				var formData = new FormData();
				formData.append('image', _this.fileToUpload);

				ApiClient.User.uploadImage(formData, function(res) 
				{
					EventsManager.fire(Events.UploadsManager.uploadFinished);
					
					APIResponse.process(res, function(res) 
					{
						EventsManager.fire(Events.UploadsManager.fileUploaded, {
							upload: res.upload
						});
					}, 
					function(res) 
					{
						PageDirector.showOperationError(res.ERRORS);
					});

				});
			}
			else 
			{
				PageDirector.showOperationError(uiText(UITexts.DataValidator.fileFormatInvalid, _this.fileToUpload));
			}
		}
		else 
		{
			PageDirector.showOperationError(uiText(UITexts.DataValidator.fileNotSelected));
		}
	};
};


Events.UploadsManager = 
{
	getUploadsStarted: "getUploadsStarted",
  getUploadsFinished: "getUploadsFinished",
  getUploadsLoaded: "getUploadsLoaded",

	uploadStarted: "uploadStarted",
	uploadFinished: "uploadFinished",
	fileUploaded: "fileUploaded",

  fileSelected: "fileSelected"
};

Events.registerEvents(Events.UploadsManager);

EventsManager.subscribe(Events.Application.appStarting, function() 
{
	Register.uploadsManager = new UserUploadsManager();
});

var UsersSearch = function() 
{
	var _this = this;
	
	this.searchData = {
		startingFrom: 0, 
		amount: 10,
		
		loadedUsers: [],
		users: [],
		totalAmount: 0
	};
	
	this.tagsData = {
		startingFrom: 0,
		amount: 100,
		
		loadedTags: [],
		totalAmount: 0,
		
		newTags: [],
		leftAmount: 0
	};
	
	this.init = function() 
	{
		_this.loadTags();
		
		EventsManager.subscribe(Events.UserInfo.userLangsChanged, function(langs) 
		{
			_this.reset();
		});
	};
	
	this.search = function() 
	{
		EventsManager.fire(Events.UsersSearch.searchReset);
		
		_this.searchData.loadedUsers = [];
		_this.searchData.startingFrom = 0;
		_this._runSearch();
	};
	
	this.moreUsers = function() 
	{
		_this.searchData.startingFrom += _this.searchData.amount;
		_this._runSearch();
	};
	
	this._runSearch = function() 
	{
		EventsManager.fire(Events.UsersSearch.searchStarted);
		
		var query = {
			accountName: this.settings.data.accountName,
			tags: this.settings.data.tags
		};
		
		ApiClient.Main.findUsers(query, _this.searchData.startingFrom, _this.searchData.amount, function(res) 
		{	
			EventsManager.fire(Events.UsersSearch.searchFinished);
			
			_this.searchData.loadedUsers = _this.searchData.loadedUsers.concat(res.users);
			_this.searchData.users = res.users;
			_this.searchData.totalAmount = res.totalAmount;
			
			EventsManager.fire(Events.UsersSearch.usersLoaded, _this.searchData);
		});
	};
	
	this.loadTags = function() 
	{
		EventsManager.fire(Events.UsersSearch.tagsLoading);
		
		var cachedTags = DataCache.get(DataCache.Keys.usersSearchTags, {
			startingFrom: -1
		});
		
		if(cachedTags.startingFrom >= _this.tagsData.startingFrom) 
		{
			cachedTags.newTags = cloneObj(cachedTags.loadedTags); 
			_this.tagsData.startingFrom = cachedTags.startingFrom;
			
			EventsManager.fire(Events.UsersSearch.tagsLoaded, cachedTags);
		}
		else 
		{
			ApiClient.Main.getPopularTags(_this.tagsData.startingFrom, _this.tagsData.amount, function(res) 
			{
				_this.tagsData.loadedTags = _this.tagsData.loadedTags.concat(res.list);
				_this.tagsData.totalAmount = res.totalAmount;
				_this.tagsData.newTags = res.list;
				_this.tagsData.leftAmount = _this.tagsData.totalAmount - _this.tagsData.loadedTags.length;

				DataCache.save(DataCache.Keys.usersSearchTags, _this.tagsData);

				EventsManager.fire(Events.UsersSearch.tagsLoaded, _this.tagsData);
			});
		}
	};
	
	this.loadMoreTags = function() 
	{
		_this.tagsData.startingFrom += _this.tagsData.amount;
		_this.loadTags();
	};
	
	this.resetTags = function() 
	{
		_this.tagsData.startingFrom = 0;
		_this.tagsData.loadedTags = [];
		
		DataCache.remove(DataCache.Keys.usersSearchTags);
		
		_this.loadTags();
	};
	
	this.settings = 
	{
		data: {
			tags: [],
		},
		
		addTag: function(tag) 
		{
			if(this.data.tags.indexOf(tag) !== -1) {
				return false;
			}
			
			if(false === this.data.tags.length < 5) {
				return false;
			}
			
			this.data.tags.push(tag);

			EventsManager.fire(Events.UsersSearch.searchSettingsChanged, {
				settings: this.data
			});
		},

		removeTag: function(tag) 
		{
			removeFromArray(this.data.tags, tag);

			EventsManager.fire(Events.UsersSearch.searchSettingsChanged, {
				settings: this.data
			});
		}
	};
};

EventsManager.subscribe(Events.UserInfo.userLangsChanged, function(langs) 
{
	DataCache.remove(DataCache.Keys.usersSearchTags);
});

Events.UsersSearch = 
{
	searchSettingsChanged: "searchSettingsChanged",
	
	searchReset: "searchReset",
	searchStarted: "searchStarted",
	searchFinished: "searchFinished",
	
	usersLoaded: "usersLoaded",
	
	tagsLoading: "tagsLoading",
	tagsLoaded: "tagsLoaded"
};

Events.registerEvents(Events.UsersSearch);

var PublicChat = function()
{
  this.join = function(chatId)
  {
    ApiClient.Chat.join(chatId, function(res)
    {
      NavigationManager.goToUrl(new NavigationManagerUrl().createFromRelative(
        "?page=chat&chatId="+res.chat.chatId,
        "/cid/"+res.chat.chatId));
    });
  };
};

Register.publicChat = new PublicChat();

var ChatsFeed = function()
{

  var _this = this;

  this.getInitialChatsFeedData = function() {
    return {
      list: [],
      totalAmount: 0
    };
  };

  this.currentChatsFeedData = this.getInitialChatsFeedData();


  this.init = function()
  {
    EventsManager.subscribe(Events.UpdatesWatcherData.chatsFeed, function(data)
    {
      _this.processFeed(data);
    });
  };

  this.refresh = function()
  {
    ApiClient.Main.getChatsFeed(function(res)
    {
      _this.currentChatsFeedData = _this.getInitialChatsFeedData();
      _this.processFeed(res);
    });
  };

  this.submit = function(data)
  {
    ApiClient.Chat.create(data, function(res)
    {
      EventsManager.fire(Events.ChatsFeed.entrySubmitted, res);
    });
  };

  this.processFeed = function(data)
  {
    var feedData = {
      all: [],
      chatsToAdd: [],
      chatsToRemove: [],

      totalAmount: 0
    };

    var currentChatIds = _this.currentChatsFeedData.list.map(function(chat) { return chat.chatId; });
    var newChatIds = [];

    performOnEveryKey(data.langs, function(lang, chatsList)
    {
      performOnElsList(chatsList.chats, function(chat)
      {
        feedData.all.push(chat);
        newChatIds.push(chat.chatId);

        if(currentChatIds.indexOf(chat.chatId) === -1) {
          feedData.chatsToAdd.push(chat);
        }
      });
    });

    performOnElsList(_this.currentChatsFeedData.list, function(chat)
    {
      if(newChatIds.indexOf(chat.chatId) === -1) {
        feedData.chatsToRemove.push(chat.chatId);
      }
    });


    _this.currentChatsFeedData.list = feedData.all;
    _this.currentChatsFeedData.totalAmount = feedData.totalAmount = newChatIds.length;

    EventsManager.fire(Events.ChatsFeed.feedUpdated, feedData);
  }
};

Events.ChatsFeed =
{
  feedUpdated: "feedUpdated",
  entrySubmitted: "entrySubmitted"
};

Events.registerEvents(Events.ChatsFeed);


EventsManager.subscribe(Events.Application.appStarting, function()
{
  Register.chatsFeed = new ChatsFeed();
  Register.chatsFeed.init();
});

EventsManager.subscribe(Events.NavigationManager.urlOpened, function(data) 
{
	if(window.location.href != data.urlInfo.global) 
	{
		EventsManager.subscribeOnce(Events.PageDirector.pageInited, function() 
		{
			if(window.ga) 
			{
				ga('set', 'page', UrlInfo.read(data.urlInfo.global).pathname);
				ga('send', 'pageview');
			} else {
				Debug.error("Google Analytics is not defined.");
			}
			
		}, { id: "GoogleAnalytics" });
	}
});

var GoogleAdmob = 
{
  status: {
    isInterstitialReady: false
  },
  
  init: function() 
  {
    var admobOptions = {};
    
    admobOptions[OSTypes.android] = {
      publisherId:          "ca-app-pub-8217034808092975~8601762875",  
      interstitialAdId:     "ca-app-pub-8217034808092975/9104535187",  
      isTesting: false
    };
    
    admobOptions[OSTypes.ios] = {
      publisherId:          "ca-app-pub-8217034808092975~9970684868",  
      interstitialAdId:     "ca-app-pub-8217034808092975/1417229815",  
      isTesting: false
    };
    
    admob.setOptions(admobOptions[Config.OS], function() {
      GoogleAdmob.prepareInterstitialAd();
    }, function(e) {
      console.log("Admob set option error. Detals: ["+JSON.stringify(e)+"]")
    });
    
    document.addEventListener(admob.events.onAdLoaded, this.onAdLoaded);
    document.addEventListener(admob.events.onAdFailedToLoad, this.onAdFailedToLoad);
  },
  
  prepareInterstitialAd: function() 
  {
    if (this.status.isInterstitialReady === false) 
    {
        admob.requestInterstitialAd({
            autoShowInterstitial: false
        });
    }
  },
  
  showInterstitialAd: function() 
  {
    if (this.status.isInterstitialReady) 
    {
      GoogleAdmob.status.isInterstitialReady = false;
      admob.showInterstitialAd(function () {});
      GoogleAdmob.prepareInterstitialAd();
    }
  },
  
  onAdLoaded: function(e) {
    console.log("Ad loaded and ready.");
    if (e.adType === admob.AD_TYPE.INTERSTITIAL) 
    {
        GoogleAdmob.status.isInterstitialReady = true;
    }
  },
  
  onAdFailedToLoad: function(e) {
    console.log("Ad failed to load. Details: ["+JSON.stringify(e)+"]");
  },
};

if(window.admob) 
{
  EventsManager.subscribe(Events.Application.appReady, function() {
    GoogleAdmob.init();
  });

  EventsManager.subscribe(Events.PageDirector.pageLoaded, function(data) {
    if(data.pageName == "user") 
    {
      GoogleAdmob.showInterstitialAd();
    }
    
    GoogleAdmob.prepareInterstitialAd();
  });
}

var GoogleAdsense = 
{
  show: function() 
  {
    (adsbygoogle = window.adsbygoogle || []).push({});
  }
};

Ajax.expectedStatuses.push(0);

EventsManager.subscribe(Events.XMLHTTP.requestFailed, function(data) 
{
	switch(data.xmlhttp.status) 
	{
		case 503:
			EventsManager.fire(Events.Application.serviceUnavailable);
			break;
			
		default:
			if(data.xmlhttp.responseURL && !data.xmlhttp.aborted) {
				
				Register.errorHandler.handle({
					message: "Empty not aborted response.",
					userData: data.xmlhttp
				}, uiText(UITexts.Application.invalidServerResponse));
			}
			break;
	}
});

EventsManager.subscribe(Events.ApiClient.response, function(res) 
{
	if(parseVersionString(AppInfo.getUIVersion()) < parseVersionString(res.uiVersion))
	{
		EventsManager.fire(Events.Application.updateRequired);
	}
});


// Temporary for users with no account
EventsManager.subscribeMulti([Events.Session.userLoaded, Events.PageDirector.pageInited], function(data) 
{
	if(Session.hasUser() 
	&& Session.userHasAccount() == false
	&& PageDirector.currentPageName !== "signup"
	&& PageDirector.currentPageName !== "delete_account") 
	{
		NavigationManager.goToUrl(new NavigationManagerUrl().createFromRelative("?page=signup", "/auth/create"));
	}
	
	if(Session.hasUser() 
	&& Session.isUserAgreementAccepted() == false
	&& PageDirector.currentPageName !== "service_agreement_updated"
	&& PageDirector.currentPageName !== "delete_account") 
	{
		NavigationManager.goToUrl(new NavigationManagerUrl().createFromRelative("?page=service_agreement_updated", "/index/service_agreement_updated"));
	}
});

EventsManager.subscribe(Events.Application.appInited, function() 
{
	try {
//		UIManager.json2html(getEl("_templates_json"));
//		UIManager.json2html(getEl("_updatable_pages_json"));
//		UIManager.json2html(getEl("IndexPageContentHandler"));
	}
	catch(e) {
		
	}
});

EventsManager.subscribe(Events.UpdatesWatcherData._root, function() 
{
	if(Register.isRobotAgent) 
	{
		UpdatesWatcher.terminate();
	}
});

AppInfo.setup.version = "2.000014";
AppInfo.setup.uiVersion = "2.00410";

AppInfo.setup.platform = PlatformTypes.browser;
AppInfo.setup.dataStorage = LocalStorage;
AppInfo.setup.sessionStorage = CookieStorage;
AppInfo.setup.OS = OSTypes.unknown;


EventsManager.subscribe(Events.Application.resetRequested, function(data) 
{
	window.location.reload();
});


/*
 * 
 * Reload page when UI language changed 
 */
EventsManager.subscribe(Events.UserInfo.uiLangChanged, function(lang) {
	location.reload();
});


/**
 * 
 * Use browser history to simulate navigation back
 */
EventsManager.subscribe(Events.PageDirector.goToPreviousPage, function() {
	window.history.back();
});


/**
 * 
 * Use browser history to simulate url change
 */
EventsManager.subscribe(Events.NavigationManager.urlOpened, function(data) 
{
	if(data.urlInfo.local && window.location.href != data.urlInfo.global) 
	{
		if(window.history && window.history.pushState) 
		{
			window.history.pushState(data.urlInfo.getForHistory(), data.urlInfo.local, data.urlInfo.global);
		}
	}
});

EventsManager.subscribe(Events.NavigationManager.urlUpdated, function(data) 
{
	if(data.urlInfo.local && window.location.href != data.urlInfo.global) 
	{
		window.history.replaceState(data.urlInfo.getForHistory(), data.urlInfo.local, data.urlInfo.global);
	}
});


/**
 * 
 * Handle browser navigation
 * 
 * @param {type} e
 * @returns {undefined}
 */
window.onpopstate = function(e) 
{
	var prevUrl = NavigationManager.getPreviousUrl();
	if(prevUrl) 
	{
		if(prevUrl.local) 
		{
			if(e.state == null || e.state.local == prevUrl.local) 
			{
				NavigationManager.goToPreviousUrl();
				return;
			}
		} else {
			if(e.state == null) {
				window.location = prevUrl.global;
			}
		}
	}
	
	var nextUrl = NavigationManager.getNextUrl();
	
	if(e.state) 
	{
		if(nextUrl && e.state.local == nextUrl.local) {
			NavigationManager.goToNextUrl();
		} else {
			NavigationManager.goToUrl(e.state)
		}
		
		return;
	}
};



/**
 * 
 * Compatibility with onld versions
 * @returns {undefined}
 */
(function migrateCookies() 
{
	var auth = AppInfo.setup.sessionStorage.get("AUTH");
	AppInfo.setup.sessionStorage.remove("AUTH", null, document.domain);
	AppInfo.setup.sessionStorage.save("AUTH", OR(auth, ""));
})();

AppInfo.setup.screen = ScreenTypes.desctop
AppInfo.setup.messagesOrder = MessagesOrder.topBottom;

AppInfo.setup.domain = "chatprostotak.com"
AppInfo.setup.baseUrl = "https://" + AppInfo.setup.domain;
AppInfo.setup.wssUrl = "wss://wss." + AppInfo.setup.domain + "/wss/";
AppInfo.setup.iconMain = '/public/img/Icons/chatprostotak/Icon.png';
AppInfo.setup.iconAlt = '/public/img/Icons/chatprostotak/Icon2.png';