// "use strict";

// --- utility functions ---
/* Debug code */
var DEBUG = 0;
var isIE = !!navigator.userAgent.match(/\smsie\s[678]/i);
var isIE67 = !!navigator.userAgent.match(/\smsie\s[67]/i);
var isIE8 = !!navigator.userAgent.match(/\smsie\s[8]/i);
var useFixForIE8 = false && isIE8;

window.onerror = function(e, file, line) {
	// For Google non-standard browser
	var msg = e.message ? (e.name + ':' + e.message) : e;
	var text = "JS page error: " + msg + ", at file " + file + ", line " + line;	
	logJSError(text);
}

function l() {
    if (DEBUG && window.console && !isIE) {
        console.log.apply(console, arguments);
    }
    
//    if (isIE) {
//    	window.logger = window.logger || document.body.appendChild(newNode('pre', { cssText: 'height: 20em; overflow: scroll;'}));
//    	for (var i = 0; i < arguments.length; i++) {
//    		window.logger.appendChild(document.createTextNode((((new Date()).getTime()) % 100000 / 1000) + 'ms: ' +  arguments[i]));
//    		window.logger.appendChild(document.createElement('br'));
//    		window.logger.scrollTop = 999999;
//    	}
//    }
}

function logJSError(e) {
    l("Logging error to server: " + e);
    e = e.length > 1500 ? e.substr(0, 1500) + '...' : e;
    (new Image()).src = window.jsConfig.reportPath + '&text=' + encodeURIComponent(e);
}

function jsErrorToString(e)
{
	var etext  = e.name + ' [' + e.message + ']';

    if (e.fileName) { etext += '\nFile "' + e.fileName + '"'; }
    if (e.lineNumber) { etext += '\nLine ' + e.lineNumber; }

    etext += '\nUA: ' + navigator.userAgent;

    if (e.stack) { etext += '\nStack: ' + e.stack; }    
    etext += "\nLocation: " + location.href;

	return etext;
}

function assert(cond) {
    if (!cond) {
        var wh = assert.caller ?
            assert.caller.toString().substring(0, 60) + '...' :
            'unknown';

        throw new Error("Assertion failed at " + wh);
    }
}

function wrap(callback, comment) {
    return function () {
        try {
            return callback.apply(this, arguments);
        } catch (e) {
            logJSError(comment ? "Error in " + comment + ': ' + jsErrorToString(e) : jsErrorToString(e));            
            throw e;
        } 
    }
}

function timer(comment) {
	var me = arguments.callee;
	var now = (new Date()).getTime();
	if (!me.log) {
		me.log = [];
	}
	
	me.log.push([now, comment || 'mark']);
}

timer.dump = function(comment) {
	if (comment) {
		timer(comment);
	}
	
	if (this.log) {
		var prev = 0;
		for (var i = 0; i < this.log.length; i++) {
			var item = this.log[i];
			l((prev ? "+" + Math.round(item[0] - prev, 3) + "ms: " : "Start: ") + item[1]);
			prev = item[0];
		}
		
		delete this.log;
	}
}


// --- Misc --- 
function bind(obj, fn) {
    return function()
    {
        return fn.apply(obj, arguments);
    };
}

function cloneObject(o) {
	if (typeof(o) != 'object') {
		return o;
	}
	
	var clone = {};
	for (var i in o) {
		if (typeof(o[i]) == 'object') {
			clone[i] = cloneObject(o[i]);
		} else {
			clone[i] = o[i];
		}	 
	}
	
	return clone;
}

function queueCall(fn /* might be null */, queueStatus, timeout) {	
	if (queueStatus.handle) {
		clearTimeout(queueStatus.handle);
		delete queueStatus.handle;
	}
	
	fn && (queueStatus.handle = setTimeout(wrap(function() {	
		fn.call(null);
	}, 'queueCall'), timeout !== undefined ? timeout : 1000));	
}

queueCall.isQueued = function (status) { return !!status.handle; }

// --- DOM & events ---
function newNode(name, properties) {
    var p = properties || {};
    var e = document.createElement(name);
    var style = 'cssText' in p ? p.cssText : '';
    delete p.cssText;

    for (var i in p) {
        e[i] = p[i];
    }

    style && (e.style.cssText = style);
    return e;
}

function createFragment(html) {
	var t = document.createElement('div');
	t.innerHTML = html;
	var f = document.createDocumentFragment();
	var ch = t.children, cl = ch.length;
	
	for (var i = 0; i < cl; i++ ) {
		f.appendChild(ch[i]);
	}
	
	return f;
}

function addEvent(obj, type, fn) {
    // We'll capture all errors this way
    var fn = wrap(fn, type + '_handler');

    if (obj.addEventListener) {
		obj.addEventListener(type, fn, false);
    } else if (obj.attachEvent) {
		obj.attachEvent("on"+type, fn);
    } else {
        throw new Error('Events attaching are not supported in this browser');
    }
    
    return [addEvent.MAGIC, obj, type, fn];
}

addEvent.MAGIC = 0xf565;

function removeEvent(handle) {
	assert(handle.length == 4);
	assert(handle[0] == addEvent.MAGIC);
	if (obj.removeEventListener) {
		obj.removeEventListener(handle[1], handle[2], handle[3]);
	} else {
		obj.detachEvent("on" + handle[1], handle[2]);
	}
}

function addHandlers(node, h) {
	for (var type in h) {
		node['on' + type] =  h[type];
	}
}

function preventEvent(e) {
	e.returnValue = false;
	e.preventDefault && e.preventDefault();
}

// If target
function isNodeInsideSecond(node, parent) {
	if (node.compareDocumentPosition) {
		return node.compareDocumentPosition(parent) & 0x10; // DOCUMENT_POSITION_CONTAINED_BY
	} else if (parent.contains) {
		// IE
		return parent.contains(node);
	} else {
		throw new Exception('Cannot use isNodeInsideSecond()');
	}
}

function findId(id) {
	return document.getElementById(id);
}

function findTag(name, context) {
	var n = (context || document).getElementsByTagName(name);
	return n ? n[0] : null;
}

/*
* Find first element by class name
*/
function find(klass, root, tagHint) {
    if (root.getElementsByClassName) {
        var e = root.getElementsByClassName(klass);
        return e ? e[0] : null;
    }

    if (root.querySelector) {
        // IE 8
        return root.querySelector('.' + klass);
    }

    // IE7-
    var tags = root.getElementsByTagName(tagHint || '*');
    var tagsCount = tags.length;
    var re = new RegExp('(^|\\s)' + klass + '(\\s|$)');
    for (var i = 0; i < tagsCount; i++) {
        if (re.test(tags[i].className)) {
            return tags[i];
        }
    }

    return null;
}

function parentTag(elm, name) {
	name = name.toUpperCase();
	
    for(;elm.parentNode && elm != document.body; elm = elm.parentNode) {
        if (elm.nodeName == name) {
            return elm;
        }        
    }

    return null;
}

function findParent(elm, klass) {
	var re = new RegExp('(^|\\s)' + klass + '(\\s|$)');
	
    for(;elm.parentNode && elm != document.body; elm = elm.parentNode) {
        if (re.test(elm.className)) {
            return elm;
        }        
    }

    return null;
}

function getPosition(node) {
	var left = 0, top = 0, body = document.body;
	
	// Count scroll, but IE8 (some versions) already counts it
	if (!useFixForIE8) {
		for (var n = node.parentNode; n && n != body; n = n.parentNode) {
			left -= n.scrollLeft;
			top -= n.scrollTop;
		}
	}
	
	// And offsets
	for (; node && node != body; node = node.offsetParent) {
		left += node.offsetLeft /*  + node.offsetParent.clientLeft */;
		top += node.offsetTop /*  + node.offsetParent.clientTop */;
	}
	
	return { x:left, y:top };
}


function addClass(elm, klass) {
	if (!hasClass(elm	, klass)) {
		elm.className += (elm.className === '' ? '' : ' ') + klass;
	}
}

function removeClass(elm, klass) {
    elm.className = elm.className.replace(new RegExp('(^|\\s)' + klass + '(\\s|$)', 'g'), ' ');
}

function hasClass(elm, klass) {
    return (new RegExp('(^|\\s)' + klass + '(\\s|$)')).test(elm.className);
}

function toggleClass(elm, klass) {
	var re = new RegExp('(^|\\s)' + klass + '(\\s|$)', 'g');
	if (re.test(elm.className)) {
		elm.className = elm.className.replace(re, ' ');
	} else {
		elm.className += (elm.className === '' ? '' : ' ') + klass;
	}
}



// --- Animation ---
function Animate(callback, options) {
    options = options || {};
    var timeStep = options.timeStep || 16;
    var fullDuration = options.fullDuration || 200;
    var value = options.start || 0;
    var from = options.from || 0;
    var to = options.to || 1;
    assert(value >= from && value <= to);
    var interval = null;
    var startValue, endValue, startTime, endTime;
    var tickFn = wrap(onTick);
    
    function isRunning() { return !!interval; }
    function getTarget() { return interval ? (endValue - value) : 0; }

    function setBounds(newFrom, newTo) {
    	assert(newTo >= newFrom);
    	if (newFrom > from) {
    		endValue = Math.max(newFrom, endValue);
    		value = Math.max(newFrom, value);
    	}
    	
    	if (newTo < to) {
    		endValue = Math.min(newTo, endValue);
    		value = Math.min(newTo, value);
    	}
    	
    	from = newFrom;
    	to = newTo;    	      
    }
    
    function stop() {
        clearInterval(interval);
        interval = null;
    }

    function jump (pos) {
        assert(pos >= from && pos <= to);
        value = pos;
        stop();
        callback && callback.call(i, pos, 1);
    }

    function goTo (target, duration) {
        assert(target >= from && target <= to);
        if (target == value) {
            jump(target);
            return;
        }
        
        startValue = value;
        endValue = target;
        startTime = (new Date()).getTime();
        // keep constant velocity
        duration = duration || (Math.abs((endValue - startValue) / (from - to)) * fullDuration);
        endTime = startTime + duration;
        assert(Math.abs(endTime - startTime) > 1e-4);

        if (!interval) {
            interval = setInterval(tickFn, timeStep);
        }
    }

    function set(val) {
        assert(!interval);
        assert(val >= from && val <= to);
        value = val;
    }

    function onTick() {
    	if (!interval) { return; } // IE bug
        var pos = ((new Date()).getTime() - startTime) / (endTime - startTime);
        pos = Math.min(1, pos);
        assert(pos >= 0 && pos <= 1);
        value = (endValue - startValue) * pos + startValue;

        if (pos == 1) {
            stop();
        }

        callback && callback.call(i, value, pos);
    }

    var i = {
        stop: stop,// Stop without calling anything
        jump: jump, // Jump immediately, stop animation, calling a callback
        goTo: goTo, // Start animation to target point or jump() if we are there
        set: set,    // Just change internal value
        isRunning: function() { return !!interval },
        getTarget: getTarget,
        getValue: function() { return value },
        setBounds: setBounds
    };

    return i;
}

// --- AJAX ---
/*
Options list: 	url, type, data, cache = false, preventCaching=true, 
				error, complete, success, start,
				timeout, headers, dataType: html|json,
				target <-- node to insert response HTML code 
				
returns: XMLHttpRequest
*/
function emptyFunction() {}

function Ajax(o) {
	var timeout = o.timeout || 30;
	var toHandle = null;
	var isPost = o.type && o.type.toLowerCase() == 'post';
	var xhr = getXHRObject();
	var preventCaching = 'preventCaching' in o ? o.preventCaching : true;  
	var cache = o.cache || false;  
    var active = false;
    var url = '';
    var params = ('data' in o) ? Ajax.serialize(o.data) : '';
    var CACHE_LIMIT = 15;
    
    if (!isPost && params !== '') {
        var haveQ = url.indexOf('?') !== -1;
        url = o.url + (haveQ ? '&' : '?') + params;
    } else {
        url = o.url;
    }
    
    var cacheUrl = url;
    
    if (!isPost && cache) {
    	var cached = getCached(cacheUrl);
    	if (cached !== null) {
    		
    		// Delay callback invocation
    		setTimeout(function () {
    			onSuccess(cached, 'Cached');
    		}, 0);   
    		
    		return;
    	}
    }
    
    if (!isPost && preventCaching) {
    	haveQ = url.indexOf('?') !== -1;
        url += (haveQ ? '&' : '?') + '_random=' + Math.round(Math.random()*100000);
    }

    if (!xhr) {
        setTimeout(function() {onError('Failed to create XMLHttpRequest object'); }, 0);
        return null;
    }

    toHandle = setTimeout(function () {
        if (active) {
            onError('Timeout expired (' + timeout + ' sec)');
        }

        if (xhr) {
            xhr.abort();
            xhr = null;
            active = null;
        }
        
    }, timeout * 1000);

    o.start && o.start(o.data || {}, xhr);

    active = true;
    xhr.open(isPost ? 'POST' : 'GET', url, true /* async */, o.username, o.password);
    xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');

    if (o.headers) {
        for (var hName in o.headers) {
            xhr.setRequestHeader(hName, o.headers[hName]);
        }
    }

    xhr.onreadystatechange = onStateChange;
    if (!isPost) {
        xhr.send(null);
    } else {
        xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        xhr.setRequestHeader("Content-length", params.length);
        xhr.setRequestHeader("Connection", "close");
        xhr.send(params);
    }

    return {
    	getXHR: function() { return xhr; },
    	abort: abort,
    	isRunning: function() { return active; }
    };  
    
    function abort() {
    	if (active && xhr) {
    		active = false; // Prevent callbacks calling
    		xhr.abort();
    	}
    	
    	xhr.onreadystatechange = emptyFunction;
        
        clearTimeout(toHandle);
        toHandle = null;
    	
        o.complete ? o.complete() : null; //(this.onComplete ? this.onComplete() : null);    	
    }
        
    // Private functions
    function onError(text) {
        active = false; // Prevent callbacks calling
        abort();
        o.error  ? o.error(text, xhr) : null; //(this.onError ? this.onError(xhr, text) : null);        
    }
    
    function onSuccess(result, status) {
    	
    	if (o.target) {
        	// Insert
        	insertHtml(o.target, result);
        }
    	
    	o.complete ? o.complete() : null; //(this.onComplete ? this.onComplete() : null);
        o.success && o.success(result, status, xhr);
    }

    function onStateChange() {
        if (active && xhr.readyState == 4) {
            active = false;
            xhr.onreadystatechange = emptyFunction;

            if (xhr.status == 200) {
                // Check for JSON
                var result;

                if (o.dataType == 'json') {
                    if (null === (result = parseJson(xhr, o.noStrictJSON))) {
                        return;
                    }
                } else {
                    result = xhr.responseText;                   
                }
                
                onSuccess(result, xhr.statusText);                
                cache && putCached(cacheUrl, result);
            } else {
                onError('Server returned code ' + xhr.status + ' ' + xhr.statusText);
            }
        }
    }
    
    function insertHtml(target, html) {
    	// No javascript parsing yet
    	target.innerHTML = html;
    }

    function parseJson(xhr, noStrict) {
        if (!noStrict && xhr.getResponseHeader('Content-Type') != 'application/json') {
            onError('Invalid Content-Type for json');
            return null;
        }

        var response = xhr.responseText;

        if (response.charAt(0) !== '{') {
            onError('JSON response should start with "{"');
            return null;
        }

        // Try to eval
        try {
            return (window.JSON && JSON.parse) ? JSON.parse(response) : eval('(' + response + ')');
        } catch (e) {
            onError('Failed to parse JSON: ' + e.toString());
            return null;
        }
    }
        
    function getXHRObject() {
        try {
            if (window.XMLHttpRequest) {
                return new XMLHttpRequest();
            }
            else if('ActiveXObject' in window) {
            //Use IE's ActiveX items to load the file.
                return new ActiveXObject("MSXML2.XMLHTTP.3.0");
            }
            else {
                return null;
            }
        }
        catch (e) { return null; }
    }
    
    // AJAX cache
    function getCached(url) {
    	if (Ajax._reqCache && Ajax._reqCache[url]) {
    		return Ajax._reqCache[url];
    	}
    	
    	return null;
    }
    
    function putCached(url, data) {
    	if (!Ajax._reqCache) {
    		Ajax._reqCache = {};
    		Ajax._reqCacheOrder = [];    		
    	} 
    	
    	if (Ajax._reqCacheOrder.length >= CACHE_LIMIT) {
    		var key = Ajax._reqCacheOrder.shift();
    		delete Ajax._reqCache[key];
    	}
    	
    	if (!(url in Ajax._reqCache)) {
    		Ajax._reqCacheOrder.push(url);
    	}
    	
    	Ajax._reqCache[url] = data;    	
    }
}

Ajax.serialize = function (data, parentKey) {
    var ts = Object.prototype.toString;

    switch (ts.call(data)) {
        case '[object Array]':
            return this._serializeArray(data, parentKey);

        case '[object Object]':
            return this._serializeHash(data, parentKey);

        default:
            // Special treatment for null
            return parentKey + '=' + (null === data ? '' : encodeURIComponent(data));
    }
};

Ajax._serializeHash = function(data, parentKey) {
    var pKey = parentKey || '';
    var result = [];

    for (var key in data) {
        var fullKey = pKey === '' ? key : parentKey + encodeURIComponent('[' + key + ']');
        var val = data[key];

        if (typeof(val) == 'string') {
            result.push(fullKey + '=' + encodeURIComponent(val));
        } else {
            result.push(this.serialize(val, fullKey));
        }
    }

    return result.join('&');
};

Ajax._serializeArray = function(data, parentKey) {
    var pKey = parentKey || '';
    var result = [];
    var l = data.length;

    for (var key = 0; key < l; key ++) {
        var fullKey = pKey === '' ? key : parentKey + encodeURIComponent('[' + key + ']');
        var val = data[key];

        if (typeof(val) == 'string') {
            result.push(fullKey + '=' + encodeURIComponent(val));
        } else {
            result.push(this.serialize(val, fullKey));
        }
    }

    return result.join('&');
};

// --- Main code ---
function attachScroller(root, options)
{
	options = options || {};
	
	function getChildren(root) {
		var r = [], children = root.childNodes, cl = children.length;
		for (var i = 0; i < cl; i++) {
			r.push(children[i]);
		}
		return r;
	}
	
	function isMouseLeave(ev) {
		var to = ev.relatedTarget || ev.toElement;
		var from = ev.target || ev.fromElement;
		return !to || !isNodeInsideSecond(to, from);
	}
	
	// Wrap root contents with scroller div, and set root overflow to hidden
	var scroller = newNode('div', { cssText: 'overflow: hidden; width: 100%;', className: 'scroller-hidden' });
	var leftButton = newNode('button', { /* Not working IE6  type: 'button', */ 'data-action': 'left', className: 'scroller-left-button', 
		innerHTML: '←' });
	var rightButton = newNode('button', { /* Not w. IE6 type: 'button', */ 'data-action' : 'right', className: 'scroller-right-button', 
		innerHTML: '→' });
	var buttonsVisible = true;
	
	var scrollLeft, scrollMax, scrollWindow, clickScrollAmount;
	scrollLeft = root.scrollLeft;
	showButtons(false);
	
	var children = getChildren(root);
	root.appendChild(leftButton);
	root.appendChild(rightButton);
	root.appendChild(scroller);
	
	scroller.scrollLeft = scrollLeft;
	// Remove overflow
	root.style.overflow = 'visible';

	// Move children
	var cl = children.length;
	for (var i = 0; i < cl; i++) {
		scroller.appendChild(children[i]);
	}
		
	calculateSize();
	
	var anim = Animate(function(value) {
		scroller.scrollLeft = value;		
	}, { fullDuration: 500 * Math.max(1, scrollMax / scrollWindow), from: 0, to: Math.max(1, scrollMax) }); 
		
	// Setup handlers
	var handlers = {
		'mousedown': wrap(mouseDown),
		'mouseup': wrap(mouseUp),
		'click': wrap(mouseClick),
		'mouseout': wrap(mouseOut)
	};
	
	addHandlers(leftButton, handlers);
	addHandlers(rightButton, handlers);	

	// Follow screen width changes
	resizeListener().add(checkSize);
	
	function calculateSize() {
		scrollMax = Math.max(0, scroller.scrollWidth - scroller.clientWidth);
		scrollWindow = scroller.clientWidth;
		clickScrollAmount = Math.ceil(scrollMax / 20);
		
		// Show/hide buttons?
		showButtons(scrollMax > 1);
	}
	
	function showButtons(enable) {
		if (enable ? !buttonsVisible : buttonsVisible) {
			buttonsVisible = enable;
			leftButton.style.display = enable ? '' : 'none';
			rightButton.style.display = enable ? '' : 'none';
		}
	}

	function checkSize() {
			
		if (scroller.clientWidth != scrollWindow || scroller.scrollWidth != scrollMax + scrollWindow) {
			calculateSize();
			anim.setBounds(0, scrollMax);
		}
	}
	
	function mouseDown(ev) {		
		var e = ev || window.event, t = e.target || e.srcElement;
		// Check scrolled area size
		checkSize();
		anim.goTo(t['data-action'] == 'right' ? (scrollMax) : 0);
	}
	
	function mouseUp() { anim.stop(); }
	
	function mouseClick(ev) {
		var e = ev || window.event, t = e.target || e.srcElement;
		var newValue = t['data-action'] == 'right' ?
			Math.min(anim.getValue() + clickScrollAmount, scrollMax) :
			Math.max(0, anim.getValue() - clickScrollAmount );
		if (!anim.isRunning()) { anim.jump(newValue); } 
	}
	
	function mouseOut(ev) {
		var e = ev || window.event;
		if (isMouseLeave(e)) {
			anim.stop();
		}
	}
	
	function ensureIsVisible(left, right) {
		if (left < scroller.scrollLeft) {
			anim.goTo(Math.max(left, 0), 200);
		} else if (right > scroller.scrollLeft + scrollWindow) {
			var d = right - scroller.scrollLeft - scrollWindow;
			anim.goTo(Math.min(scrollMax, scroller.scrollLeft + d), 200);
		}
	}
	
	return { claculateSize: calculateSize, ensureIsVisible: ensureIsVisible };
}


function attachAjaxCatalog(root, initialFilters)
{
	var resultsArea;
	var filters;
	var indicatorArea = null;
	var emptyFilter = { loc: {}, cat: {}, p: 1 };
	var doc = document.documentElement; 
	var body = document.body;
	var pagesCount = null;
	var lastToggledButton = null; // Quick fix
	
	function scrollToTop() {
		pageScroller().scrollToElement(root, -60, 350);		
	}
	
	function getDefaultFilter() {
		var filters = cloneObject(initialFilters);
		filters.loc = keysFromArray(filters.loc);
		filters.cat = keysFromArray(filters.cat);
		return filters;
	}
					
	function clearAreaCache() {
		resultsArea = null;
		indicatorArea = null;
		pagesCount = null;
	}
	
	function findResultsArea() {
		return resultsArea ? resultsArea : (resultsArea = find('js-results-area', root, 'div'));
	}
	
	function findStatusArea(which) {
		return find(which == 'bottom' ? 'js-load-status-bottom' : 'js-load-status-top', root, 'div');
	}
	
	// Intercept filter and pager clicks
	addEvent(root, 'click', wrap(function(ev) {
		var e = ev || window.event, t = e.target || e.srcElement;
		var node = parentTag(t, 'a');
		var type = null;
		var id = null;
		
		if (!node) { return; }
		
		if (id = node.getAttribute('data-loc')) {		
			var button = parentTag(node, 'li');
			click({ loc: id, p: 1 }, 'top');	
			
			clearButtons(button);
			toggleButton(button);
			
			preventEvent(e);
		}
		
		if (id = node.getAttribute('data-cat')) {
			var button = parentTag(node, 'li');
			click({ cat: id, p: 1 }, 'top');
			
			clearButtons(button);
			toggleButton(button);
			
			preventEvent(e);
		}
		
		if (id = node.getAttribute('data-page')) {
			if (id != filters.p) {				
				click({ p: id }, 'top');
				scrollToTop();
			}
			
			preventEvent(e);			
		}
		
		if (id = node.getAttribute('data-reset')) {
			filters = { cat: {}, loc: {}};
			click({ p: 1}, 'top');
			scrollToTop();
			preventEvent(e);
		}
	}));
	
	
	// Init catalog frame 
	var status = { changed: false }; 
	filters = parseSearchHash(location.hash, status);
	var currentHash = location.hash; 
	
	if (status.changed) {
		startLoading('top');
	}
	
	// Detect URL change
	setInterval(function() {
		if (location.hash != currentHash) {
			// l('Hash change detected');
			var status = { changed: false }; 
			filters = parseSearchHash(location.hash, status);
			currentHash = location.hash;
			scrollToTop();
			startLoading('top');			
		}
	}, 500);
	
	var reqTimeout, ajaxReq;
	var THROTTLE_TIME = 1000; // Protect server from overload
	var lastRequest = 0;
	
	return {
		nextPage: function() { 
			if (filters.p < getPagesCount()) { click({p: parseInt(filters.p) + 1}, 'top'); scrollToTop(); } 
		},
		prevPage: function () {
			if (filters.p > 1) { click({ p: parseInt(filters.p) - 1}, 'top'); scrollToTop(); } 			
		} 
	};
	
	function getPagesCount() {
		if (!pagesCount) {
			var div = findId('js-catalog-pages-count');
			pagesCount = (div && parseInt(div.getAttribute('data-pages'))) || 1;
		}
		
		return pagesCount;
	}
	
	function updateHash(hash) {
		location.hash = hash;
		currentHash = hash;
	}
	
	function click(params, targetArea) 
	{
		// Update filters
		if (params.p) {
			filters.p = parseInt(params.p);			
		}
		
		var k;
		
		// Add/remove flag
		if (params.loc) {						
			if (params.loc in filters.loc) {
				// delete filters.loc[params.loc];
				filters.loc = {};
			} else {
				filters.loc = {};
				filters.loc[params.loc] = true;
			}
		}
		
		// Same
		if (params.cat) {
			if (params.cat in filters.cat) {
				// delete filters.cat[params.cat];
				filters.cat = {};
			} else {
				filters.cat = {};
				filters.cat[params.cat] = true;
			}
		}
		
		var hash = buildSearchQueryString(filters);
		updateHash(hash !== '' ? '#' + hash : '');
		
		startLoading(targetArea);		
	}

		
	function startLoading(targetArea) {
					
		// If there werea request or timer, wait a little
		var shouldWait = (((new Date()).getTime() - lastRequest) < THROTTLE_TIME) || reqTimeout;

		// Abort request or queued one if there're any
		clearTimeout(reqTimeout);
		if (ajaxReq) {
			ajaxReq.abort();
			ajaxReq = null;
		}		
		
		if (shouldWait) {
			l('Setting pause before request');
			reqTimeout = setTimeout(wrap(function () { startRequest(targetArea); }), THROTTLE_TIME);
			// Show load flag 
			showIndicator(targetArea);
		} else {
			startRequest(targetArea);
		}		
	}
	
	function startRequest(targetArea) {
		
		lastRequest = (new Date()).getTime();
		reqTimeout = null;
		// Sort for better caching
		var params = {
			p: parseInt(filters.p),
			cat: arrayKeys(filters.cat).sort().join(','),
			loc: arrayKeys(filters.loc).sort().join(',')
		};
		
		ajaxReq = new Ajax({
			url: window.jsConfig.siteRoot + '/frontpage/catalog',
			data: params,
			cache: true,
			timeout: 15,
			start: function () { showIndicator(targetArea); },
			complete: function() { hideIndicator(); },
			error: function() {
				ajaxReq = null;				
				showErrorMessage();
				hideIndicator();
			},
			success: function(html) {
				ajaxReq = null;				
				root.innerHTML = html;
				clearAreaCache();
				hideIndicator();							
			}	
		});
	}	
	
	function toggleButton(obj) {
		toggleClass(obj, 'active');
	}
	
	function clearButtons(button) {
		var filter = parentTag(button, 'ul');
		for (var ch = filter.firstChild; ch; ch = ch.nextSibling) {
			if (ch != button && ch.nodeName == 'LI') {
				if (hasClass(ch, 'active')) {
					removeClass(ch, 'active');
				}
			}
		}
	}

	
	function buildSearchQueryString(filter) {
		var h = '';
		var locKeys = arrayKeys(filter.loc); 
		if (locKeys.length) {
			h += 'loc=' + arrayKeys(filter.loc).join(',');
		}
		
		var catKeys = arrayKeys(filter.cat);
		if (catKeys.length) {
			h += (h == '' ? '' : '&') + 'cat=' + catKeys.join(',');
		}
		
		if (filter.p && filter.p != 1) {
			h += (h == '' ? '' : '&') + 'p=' + filter.p;
		}
		
		return h ? h : 'empty';
	}
	
	function parseSearchHash(hash, status) {
		var filter = cloneObject(emptyFilter);
		var isSet = false;
		
		if (hash.charAt(0) == '#') {
			hash = hash.substr(1);		
		}
		
		if (hash == 'empty') {
			status.changed = true;
			return getDefaultFilter();
		}
		
		var params = hash.split('&');
		for (var i in params) {
			var parts = params[i].split('=', 2);
			if (parts.length == 2) {
				var val = parts[1];
				
				switch (parts[0]) {
					case 'p':	
						isSet = true;
						if (val != 1 && val) {
							filter.p = parseInt(val);							
						}
						break;
					case 'cat':
						isSet = true;
						if (val) {
							filter.cat = keysFromArray(val.split(','));
						}
						break;
					case 'loc':
						isSet = true;
						if (val) {
							filter.loc = keysFromArray(val.split(','));
						}
						break;
				}
			}
		}
		
		status.changed = isSet;
		return isSet ? filter : getDefaultFilter();
	}

	
	function showIndicator(area) {
		if (indicatorArea == area) {
			return;
		}
		indicatorArea = area;
		findResultsArea().style.visibility = 'hidden';
		var s = findStatusArea(area);
		if (s) {
			s.innerHTML = 'Идет поиск&hellip;';
			s.style.display = 'block';
		}
	}
	
	function hideIndicator() {
		if (indicatorArea) {
			findResultsArea().style.visibility = '';
						
			var s = findStatusArea(indicatorArea);
			if (s) {
				s.style.display = '';
			}
			
			indicatorArea = null;
		}
	}	
	
	function showErrorMessage() {
		var msg = findId('catalog-error-tpl').text || 'Ошибка загрузки. Вы можете попробовать обновить страницу.';
		findResultsArea().innerHTML = msg;
	}
	
	function arrayKeys(arr) {
		var k = [];
		for (var i in arr) {
			k.push(i);
		}
		
		return k;
	}
	
	function keysFromArray(arr) {
		var r = {};
		for (var i = arr.length - 1; i >= 0 ; i--) {
			if (arr[i]) {
				r[arr[i]] = true;
			}
		}
		
		return r;
	}
}

function setupPlaceholders(root)
{
    var t = (new Date()).getTime();
    root = root || document.body;
    if (root.addEventListener) {
        root.addEventListener('focus', beforeFocus, true);
        //root.addEventListener('keydown', beforeFocus, false);
        root.addEventListener('blur', beforeBlur, true);
    } else {
        root.attachEvent('onfocusin', beforeFocus);
        root.attachEvent('onfocusout', beforeBlur);
    }

    var labels = root.getElementsByTagName('label');
    var placeholders = {};
    var re = /(^|\s)placeholder(\s|$)/;
    for (var i = labels.length - 1; i >= 0; i--) {
        var l = labels[i];
        if (l.htmlFor && re.test(l.className)) {
            placeholders[l.htmlFor] = l;
        }
    }

    var inputs = root.getElementsByTagName('input');
    var tas = root.getElementsByTagName('textarea');
    for (var i = inputs.length - 1; i >= 0; i--) {
        var inp = inputs[i];
        if (inp.type == 'text' && inp.id && placeholders[inp.id]) {
            var isFocused = (document.activeElement == inp);  //Wouldn't work everywhere
            placeholders[inp.id].style.display = (inp.value === '' && !isFocused) ? 'block' : 'none';
        }
    }
    
    for (var i = tas.length - 1; i >= 0; i--) {
        var ta = tas[i];
        if (ta.id && placeholders[ta.id]) {
            var isFocused = (document.activeElement == ta);  //Wouldn't work everywhere
            placeholders[ta.id].style.display = (ta.value === '' && !isFocused) ? 'block' : 'none';
        }
    }

    function beforeFocus(ev) {
        var e = ev || event;
        var t = e.target || e.srcElement;
        var isRightOne = t &&  ((t.nodeName == 'TEXTAREA') ||  (t.nodeName == 'INPUT' && t.type == 'text'));

        if (isRightOne && t.id && placeholders[t.id]) {
            var p = placeholders[t.id];
            p.style.display = 'none';
        }
    }

    function beforeBlur(ev) {
        var e = ev || event;
        var t = e.target || e.srcElement;
        var isRightOne = t &&  ((t.nodeName == 'TEXTAREA') ||  (t.nodeName == 'INPUT' && t.type == 'text'));

        if (isRightOne && t.id && placeholders[t.id]) {
            var p = placeholders[t.id];
            if (t.value == '') {
                p.style.display = 'block';
            }
        }
    }
}

function attachTooltip(root, options) {
	
	options = options || {};
	
	var 
		elmClass = 'tooltip',
		attrName = 'data-tooltip', 
		popupClass = options.popupClass || 'tooltip-frame',
		popupVisible = false,
		showHideQueue = {},
		showHideTimeout = 500,
		offsetX = 0,
		offsetY = 0,
		switchTimeout = 50,		
		popupNode = null,
		popupInner = null,
		activeNode = null,
		lastHovered = null;
	
	var parentRe = new RegExp('(?:^|\\s)(?:' + elmClass + '|' + popupClass  + ')(?:\\s|$)'); 	
	var tooltipRe = new RegExp('(?:^|\\s)' + elmClass + '(?:\\s|$)');
	
	var popupAnimation = Animate(function(val) {

		assert(popupNode);
		
		setPopupOpacity(popupNode, val);
		popupNode.style.marginTop = Math.round(30 - val * 30) + 'px';
		if (!popupVisible && val > 0) {
			popupNode.style.display = 'block';
			popupVisible = true;
			
		} else if (popupVisible && val == 0) {
			popupNode.style.display = 'none';
			popupVisible = false;
		}
					
	}, { fullDuration: 200 });
		
	addEvent(root, 'mouseover', onMOver);
	addEvent(root, 'mouseout', onMOut);
	
	return;
	
	function onMOver(ev) {

		var e = ev || window.event;
		var t = e.target || e.toElement;	
		
		if (!t) {
			lastHovered = null;
			queueCall(updatePopupState, showHideQueue, showHideTimeout);
			return;
		}
		
		var node = findClosest(t, root, parentRe);

		if (lastHovered == node) {
			return;
		}
		
		
		lastHovered = node;		
		
		if (!node) {
			// hide
			queueCall(updatePopupState, showHideQueue, showHideTimeout);
			
		} else if (isTooltip(node)) {

			if (node != activeNode) {
				// Check if node exists
				if (hasContent(node)) {
					queueCall(updatePopupState, showHideQueue, switchTimeout);
				} else {
					lastHovered = null;
					queueCall(updatePopupState, showHideQueue, showHideTimeout);
				}
			} else {
				queueCall(null, showHideQueue);
			}
		} else {
			// Is a popup
			lastHovered = activeNode;
			queueCall(null, showHideQueue);
		}		
	}
	
	function onMOut(ev) {
		var e = ev || window.event;
		var t = e.relatedTarget || e.toElement;			
		
		if (!t) {
			lastHovered = null;
			queueCall(updatePopupState, showHideQueue, showHideTimeout);
			return;
		}
		
		var node = findClosest(t, root, parentRe);		
		if (lastHovered == node) {
			return;
		}
									
		if (!node) {
			lastHovered = null;
			queueCall(updatePopupState, showHideQueue, showHideTimeout);
			return;
		}
	}
	
	function updatePopupState() {
		if (!lastHovered && activeNode) {
			// Hide popup
			popupAnimation.goTo(0);
			activeNode = null;
			
		} else if (lastHovered && lastHovered != activeNode) {
			
			// Show or switch popup			
			var content = getPopupContent(lastHovered);
			if (!popupNode) {
				createPopup();
			}
			
			popupAnimation.jump(0); // hide previous one
			
			// Set new content and location
			var pos = getPopupPosition(lastHovered);
			popupNode.style.left = pos.x + 'px';
			popupNode.style.top = pos.y + 'px';
			popupInner.innerHTML = getPopupContent(lastHovered);  
			
			activeNode = lastHovered;
			popupAnimation.goTo(1);
		}
	}	
	
	function findClosest(node, root, parentRe) {
		var body = document.documentElement;
		for ( ; node && node != root && node != body; node = node.parentNode) {
			if (parentRe.test(node.className)) {
				return node;
			}						
		}
		
		return null;
	}
	
	function hasContent(node) {
		var id = node.getAttribute(attrName);
		return findId(id) ? true : false;
	}
	
	function isTooltip(node) {
		return tooltipRe.test(node.className);
	}
	
	function getPopupContent(node) {
		var id = node.getAttribute(attrName);
		var src = id && findId(id);
		return src ? src.innerHTML : '';			
	}
	
	function getPopupPosition(node) {
		var pos = getPosition(node);
		return {
			x: Math.max(0, pos.x + offsetX),
			y: Math.max(0, pos.y + node.offsetHeight + offsetY) 
		}
	}

	function setPopupOpacity(elem, value) {
	    assert(value >= 0 && value <= 1);
	    var s = elem.style;

	    // No cookies for IE
	    /* if (elem.filters) {
	        elem.filters.item("alpha").opacity = Math.round(value * 100);
	    } else */
	    if ('opacity' in s) {
	        s.opacity = value;
	    }
	}
	
	function createPopup() {
		assert(!popupNode);
		popupNode = newNode('div', { className: popupClass, innerHTML: '<div class="inner"></div>',
			cssText: 'display: none; position: absolute; z-index: 100;' });
		popupInner = find('inner', popupNode, 'div');
		addEvent(popupNode, 'mouseout', onMOut);
		addEvent(popupNode, 'mouseover', onMOver);
		document.body.appendChild(popupNode);
	}
}

function attachSlideshow(root, options) {
	var 
		back = find('background', root, 'div'),
		middle = find('middle', root, 'div'),
		front = find('front', root, 'div'),
		backInner = back,
		imageObjects = {},
		imagesLoaded = {},
		failedThumbs = {},
		loadingSrc = null,
		loadingTitle = null,
		midImage = null,
		frontImage = findTag('img', front),
		backstageError = '<span class="back-error">Ошибка при загрузке картинки. ' +
			'<br>Вы можете попробовать обновить страницу.</span>',
		backstageLoading = '<span class="back-loading">Загрузка&hellip;</span>';
	
	var thumbs = find('js-thumbs-list', root, 'ul');
	var scrollDiv = useFixForIE8 ? find('scroller-hidden', root, 'div') : null;
	if (!thumbs) {
		return;
	}
	
	var backAnim = Animate(function (val) {		
		setOpacity(back, val);
	}, { start: 0, fullDuration: isIE67 ? 50 : 300 });

	var midAnim = Animate(function (val) {		
		setOpacity(middle, val);
		if (val == 1) {
			onMidShown();
		}
	}, { start: 0, fullDuration: isIE67 ? 50 : 900 });	
	
	var frontAnim = Animate(function (val) {
		setOpacity(front, val);
		if (val == 0) {
			onFrontFaded();
		}
	}, { start: 1, fullDuration: isIE67 ? 30 : 900 });
	
	var activeThumb = findRightTag(thumbs.firstChild, 'li');
	
	addEvent(thumbs, 'click', function (ev) {
		var e = ev || window.event;
		var t = e.target || e.srcElement;
		var node = parentTag(t, 'a');
		
		if (!node) {
			return;
		}
		
		if (clickThumb(node)) {		
			preventEvent(e);
		}
	});
	
	function clickThumb(node) {		
		if (node.href) {
			loadImage(null, node.href, node.title);
			activeThumb = node;
			
			// Ensure image is visible
			if (options.scroller) {
				var li = parentTag(node, 'li');
				var left = li.offsetLeft - 55;
				var right = li.offsetLeft + li.offsetWidth + 55;
				// Fix
				if (useFixForIE8 && scrollDiv) { left += scrollDiv.scrollLeft; right += scrollDiv.scrollLeft;  }
				if (li) { options.scroller.ensureIsVisible(left, right);}
			}		
			
			return true;
		}
		
		
		return false;
	}
	
	var me = {
		loadImage: loadImage,
		nextImage: function () { 
			var li = parentTag(activeThumb, 'li'); 
			if (!li) { return ; } 
			
			var nextLi = findRightTag(li.nextSibling, 'li');
			if (!nextLi) { return; }
			
			var a = findTag('a', nextLi);
			clickThumb(a);
		},
		
		prevImage: function () { 
			var li = parentTag(activeThumb, 'li'); 
			if (!li) { return ; } 
			
			var prevLi = findLeftTag(li.previousSibling, 'li');
			if (!prevLi) { return; }
			
			var a = findTag('a', prevLi);
			clickThumb(a);			
		}
	};
	
	return me;
	
	function findRightTag(node, name) {
		name = name.toUpperCase();
		for ( ;node && node.nodeName != name; node = node.nextSibling) {};
		return node;
	}
	
	function findLeftTag(node, name) {
		name = name.toUpperCase();
		for ( ;node && node.nodeName != name; node = node.previousSibling) {};
		return node;
	}	
	
	function loadImage(idUnused, src, title) {
		// Fade front image, if other image is here, start showing it	
		if (src == loadingSrc) {
			return;	
		}
		loadingSrc = src;
		loadingTitle = title;
		frontAnim.goTo(0);
		
		if (imagesLoaded[src]) {
			backAnim.goTo(0);
			// midAnim.jump(0);
			setImage(getMidImage(), loadingSrc, loadingTitle);			
			midAnim.goTo(1);
		} else {			
			midAnim.goTo(0);
			
			setBackstageText(backstageLoading);
			backAnim.goTo(1);
		}
		
		if (!imagesLoaded[src] && !imageObjects[src]) {
			startLoading(src);
		}	
	}
	
	function startLoading(src) {
		assert(!imagesLoaded[src]);
		assert(!imageObjects[src]);
		
		var i = new Image();
		i.onload = wrap(function() { onLoad(src) }, 'image_onload');
		i.onerror = wrap(function() { onError(src) }, 'image_onerror');
		i.src = src;
		
		imageObjects[src] = i;
	}
	
	function onFrontFaded() {

	}
	
	function onMidShown() {
		// Switch with front
		frontAnim.jump(0);
		setImage(frontImage, loadingSrc, loadingTitle);
		frontAnim.jump(1);
		midAnim.jump(0);
	}
	
	function onLoad(src) {
		
		imagesLoaded[src] = true;
		delete imageObjects[src];		
		
		if (src == loadingSrc) {		
			// midAnim.jump(0);
			setImage(getMidImage(loadingSrc), loadingSrc, loadingTitle);
			
			backAnim.goTo(0);
			midAnim.goTo(1);
			
			setThumbnailFailed(activeThumb, src, false);
		}
	}
	
	function onError(src) {

		delete imageObjects[src];
		
		// If we were waiting for this one
		if (src == loadingSrc) {
			setBackstageText(backstageError);
			backAnim.goTo(1);
			
			// Mark thumbnail
			setThumbnailFailed(activeThumb, src, true);
		}
	}
	
	function setThumbnailFailed(thumb, src, hasFailed) {
		if (hasFailed && !failedThumbs[src]) {
			failedThumbs[src] = thumb;
			addClass(thumb, 'image-error');
		} else if (!hasFailed && failedThumbs[src]) {			
			removeClass(failedThumbs[src], 'image-error');
			delete failedThumbs[src];
		}
	}
	
	function getMidImage() {
		if (!midImage) {
			midImage = newNode('img', {});
			middle.appendChild(midImage);
		}
		
		return midImage;
	}
	
	function setBackstageText(text) {
		backInner.innerHTML = text;
	}
	
	function setImage(img, src, title) {
		img.src = src;
		img.title = title;
		img.alt = title;
		
//		var newImage = newNode('img');
//		newImage.src = src;
//		newImage.alt = title;
//		newImage.title = title;
//		
//		img.parentNode.replaceChild(newImage, img);
	}
	
	function setOpacity(obj, value) {
		assert(value >= 0 && value <= 1);
        var s = obj.style;
        if (value == 0) {
            s.visibility = 'hidden';
            return;
        } else if (s.visibility != 'visible') {
            s.visibility = 'visible';
        }

        if (obj.filters && obj.filters.length > 0) {
            obj.filters.item("alpha").opacity = Math.floor(value * 100);
        } else if ('opacity' in s) {
            s.opacity = value;
        }		
	}
}

function attachCalendar(input, options) {

	options = options || {};
	var
		date = null,
		monthId = null,
		dayActive = null,
		calendar = null,
		monthList = null,
		monthNameArea = null,
		daysArea = null,
		offsetLeft = -1, // for border
		offsetTop = 0,
		daysInMonth = 0,
		firstDayOffset = 0,
		visible = false;

	var
		minDate = options.minDate ? getDayStart(options.minDate) : null,
		maxDate = options.maxDate ? getDayStart(options.maxDate) : null,
		relatedInput = options.relatedInput || null;

	var now = new Date();
	var currentYear = now.getFullYear();
	var daysTableTemplate = getTemplate('tpl-days-table');

	var monthNamesForList = [
		'Янв.', 'Февр.', 'Март', 'Апр.', 'Май', 'Июнь', 'Июль', 'Авг.', 'Сент', 'Окт.', 'Нояб.', 'Дек.'
	];

	var monthNamesTitle = [
	    'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь',
	    'Ноябрь', 'Декабрь'
	];

	var monthNamesDate = [
	   	'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября',
	   	'ноября', 'декабря'
	];

	addEvent(input, 'focus', function(ev) {
		showCalendar();
	});

	addEvent(input, 'blur', function(ev) {
		hideCalendar();
	});

	if (input == document.activeElement) {
		showCalendar();
	}

	// Attach to button
	var parent = findParent(input, 'f-wrap');
	var button = parent && findTag('button', parent);
	if (button) {
		addEvent(button, 'click', function() {
			input.focus();
		});
	}

	var showAnim = Animate(function(val) {
		calendar.style.marginTop = Math.round(20 * (1 - val)) + 'px';
		setCalOpacity(calendar, val);
	}, { fullDuration: 150 });

	return;

	// Now go functions
	function setCalOpacity(obj, value) {
		assert(value >= 0 && value <= 1);
        var s = obj.style;
        if (value == 0) {
            s.display = 'none';
            return;
        } else if (s.display != 'block') {
            s.display = 'block';
        }

        if (obj.filters && obj.filters.length > 0) {
            obj.filters.item("alpha").opacity = Math.round(value * 100);
        } else if ('opacity' in s) {
            s.opacity = value;
        }
	}


	function showCalendar() {
		if (visible) { return;	}

		var d = getDate();
		var m = getMonthId(d);

		if (!calendar) {
			createCalendar(d, m);
		}

		if (m != monthId) {
			updateMonth(m);
		}

		if (d != date) {
			updateDay(d);
		}

		// Set position and visibility
		var pos = getPosition(input);
		calendar.style.top = pos.y + input.offsetHeight + offsetTop + 'px';
		calendar.style.left = pos.x + offsetLeft +  'px';
		calendar.style.minWidth = input.offsetWidth + 'px';
		showAnim.goTo(1);
		visible = true;
	}

	function hideCalendar() {
		if (!visible) {
			return;
		}

		showAnim.goTo(0);
		visible = false;
	}

	function onCalendarClick(ev) {
		var e = ev || window.event;
		var t = e.target || e.srcElement;
		var mId = parseInt(t.getAttribute('data-m'));
		preventEvent(e); // prevent losing focus

		if (mId) {
			if (minDate && getMonthId(minDate) > mId) { return; }
			// Choose month
			if (mId != monthId) {
				updateMonth(mId);
			}

			return;
		}

		if (hasClass(t, 'close-button')) {
			input.blur();
			hideCalendar();
			return;
		}

		if (t.nodeName == 'TD') {
			var d = parseInt(t.innerHTML, 10);
			if (d >= 1 && d <= 31) {
				var newDate = (new Date(Math.floor(monthId / 12), monthId % 12, d)).getTime();
				updateDay(newDate);
				input.value = getTextDate(newDate);
				hideCalendar();
				return;
			}
		}
		
		// Prevent IE from losing focus
		if (isIE67) {
			setTimeout(function() { input.focus(); }, 0);
		}
	}

	function getTextDate(d) {
		var date = (new Date(d));
		var text = date.getDate() + ' ' + monthNamesDate[date.getMonth()];
		if (date.getFullYear() != (new Date(now)).getFullYear()) {
			text += ' ' + date.getFullYear();
		}

		return text;
	}

	function createCalendar(day, monthId) {
		var code = getTemplate('tpl-calendar');
		assert(code);
		var calendarWrap = newNode('div', { className: 'b-calendar-wrapper' });
		calendarWrap.innerHTML = code;
		calendar = find('b-calendar', calendarWrap, 'div');
		calendar.style.position = 'absolute';

		daysArea = find('days-area', calendar, 'div');
		monthList =  find('month-list', calendar, 'ul');
		monthNameArea = find('month-name', calendar, 'h2');
		addEvent(calendar, 'mousedown', onCalendarClick);

		document.body.appendChild(calendar);
	}

	function updateMonth(newMonthId) {
		var fromMid = Math.max(getMonthId(minDate), newMonthId - 3);
		var html = '';
		for (var m = fromMid; m < fromMid + 7; m++) {
			var y = Math.floor(m / 12);
			var mname = monthNamesForList[m % 12];
			html += '<li tabindex=0 data-m="' + m + '"' + (m == newMonthId ? ' class="active" ' : '') + '>'
				+ mname + (y != currentYear ? " " + y : '') + '</li>';
		}

		html += '<li tabindex=0 data-m="' + m + '">дальше »</li>';
		monthList.innerHTML = html;

		// Title
		var y = Math.floor(newMonthId / 12);

		monthNameArea.innerHTML = monthNamesTitle[newMonthId % 12] + (y != currentYear ? ' ' + y : '');
		monthId = newMonthId;

		// days area
		updateDaysArea(monthId);
	}



	function updateDaysArea(m) {
		var code = daysTableTemplate;
		var html = '';
		// Calculate days in month number & offset
		var mStart = new Date(Math.floor(m/ 12), m % 12, 1);
		var nextMStart = new Date(Math.floor(m / 12) + ((m % 12 == 11) ? 1 : 0), (m + 1) % 12, 1);
		var currDate = new Date(date);
		var minDateObj = new Date(minDate);
		// chosen day number
		dayActive = (currDate.getFullYear() * 12 + currDate.getMonth()) == monthId ? currDate.getDate() : null;
		var today = ((now.getFullYear() * 12 + now.getMonth() == monthId) ? now.getDate() : null);
		var minDay = null;
		var minMonthId = minDateObj.getFullYear() * 12 + minDateObj.getMonth();
		if (m < minMonthId) {
			minDay = 32; // fade all
		} else if (m == minMonthId) {
			minDay = minDateObj.getDate();
		}
		 
		monthId = m;

		daysInMonth = Math.round((nextMStart.getTime() - mStart.getTime()) / 86400000);
		firstDayOffset = mStart.getDay() == 0 ? 6 : mStart.getDay() - 1;
		var rows = (daysInMonth + firstDayOffset) / 7;

		for (var row = 0; row < rows; row ++) {
			var rowHtml = '';
			for (var col = 0; col < 7; col++) {
				var d = -firstDayOffset + 1 + row * 7 + col;
				if (d < 1 || d > daysInMonth) {
					rowHtml += "<td data-m=" + (d < 1 ? monthId - 1 : monthId + 1) + ">&nbsp;</td>";
				} else {
					var klass = (d === dayActive ? 'active' : '');
					if (col >= 5) { klass += (klass === '' ? '' : ' ') + 'hldy'; }
					if (d === today) { klass += (klass === '' ? '' : ' ') + 'today'; }
					if (minDay && d < minDay) { klass+= (klass === '' ? '' : ' ') + 'disabled'; }
					rowHtml += "<td" + (klass ? ' class="' + klass + '"' : '') +">" + d + "</td>";
				}
			}
			html += '<tr>' + rowHtml + '</tr>';
		}

		code = code.replace('%rows%', html);
		daysArea.innerHTML = code;
	}

	function updateDay(newDate){
		if (getMonthId(newDate) != monthId) {
			return;
		}
		var newDay = (new Date(newDate)).getDate();
		if (newDay == dayActive) {
			return;
		}

		// hide prev. active day
		var table = findTag('table', daysArea);
		if (dayActive) {
			removeClass(findCell(table, dayActive), 'active');
		}
		addClass(findCell(table, newDay), 'active');

		dayActive = newDay;
	}

	function findCell(table, day) {
		var cellNo = day + firstDayOffset - 1;
		var rowNo = Math.floor(cellNo / 7) + 1;
		if (table.rows[rowNo]) {
			return table.rows[rowNo].cells[cellNo % 7];
		}

		return null;
	}

	function getDayStart(ts) {
		var d = new Date(ts);
		return (new Date(d.getFullYear(), d.getMonth(), d.getDate())).getTime();
	}

	function getMonthId(ts) {
		var d = new Date(ts);
		return d.getFullYear() * 12 + d.getMonth();
	}

	function getDate() {
		var val = input.value.toLowerCase();
		var d = parseDate(val);
		return d ? d : getDayStart((new Date()).getTime());
	}

	function parseDate() {
		// parse input value
		var val = input.value.toLowerCase();
		// 12 march '10, 6jan2010, 5 sep. 2010, jan 2010
		var wordRe = /^\s*([0123]?[0-9])?\s*(([a-zA-Zа-яА-ЯёЁ]+)\s*[.]?)(\s*'?(20[0123][0-9]|[0123][0-9]))?\s*$/;
		// 23.4.2010, 12/4/10, 24.07
		var numRe = /^\s*([0123]?[0-9])\s*[./-]\s*([01]?[0-9])(\s*[./-]\s*(20[0123][0-9]|[0123][0-9]))?\s*$/;
		// 2010-04-01
		var revNumRe = /^\s*(20[0123][0-9])\s*[./-]\s*([01]?[0-9])\s*[./-]\s*([0123]?[0-9])\s*$/;
		var months = {
			'янв': 1, 'ян': 1, 'января': 1, 'jan': 1, 'january': 1,
			'фев': 2, 'февр': 2, 'февраля': 2, 'feb': 2, 'febr': 2, 'february': 2,
			'март': 3, 'мар': 3, 'марта': 3, 'mar': 3, 'march': 3,
			'апр': 4, 'апреля': 4, 'апрель': 4, 'apr': 4, 'april': 4,
			'мая': 5, 'май': 5, 'may': 5,
			'июн': 6, 'июня': 6, 'июнь': 6, 'jun': 6, 'june': 6,
			'июл': 7, 'июля': 7, 'июль': 7, 'jul': 7, 'jule': 7,
			'авг': 8, 'август': 8, 'августа': 8, 'aug': 8, 'august': 8,
			'сен': 9, 'сент': 9, 'сентяб': 9, 'сентябрь': 9, 'сентября': 9,
			'sep': 9, 'sept': 9, 'september': 9,
			'окт': 10, 'октяб': 10, ' октябрь': 10, 'октября': 10, 'oct': 10, 'october': 10,
			'ноя': 11, 'нояб': 11, 'ноябрь': 11, 'ноября': 11, 'nov': 11, 'novermber': 11,
			'дек': 12, 'декаб': 12, 'декабрь': 12, 'dec': 12, 'december': 12
		};

		var m = [];
		var day = null, mon = null, year = null;
		var now = new Date();

		if ((m = wordRe.exec(val))) {
			day = m[1] ? m[1] : null;
			mon = (m[3] in months) ? months[m[3]] : null;
			year = m[5];
		} else if (m = numRe.exec(val)) {
			day = m[1];
			mon = m[2];
			year = m[4];
		} else if (m = revNumRe.exec(val)) {
			day = m[3];
			mon = m[2];
			year = m[1];
		}

		mon = parseInt(mon, 10);
		day = parseInt(day, 10);
		year = parseInt(year, 10);

		// If date is not valid, use current date
		if (mon >= 1 && mon <= 12) {
			if (!day || (day >= 1 && day <= 31)) {
				if (!year || (year >= 0 &&  year <= 38) || (year >= 2000 && year <= 2038)) {
					day = day || 10;
					if (!year) {
						year = (mon < now.getMonth() + 1) ? now.getFullYear() + 1 : now.getFullYear();
					}

					year = (year < 100) ? 2000 + year : year;
					date = (new Date(year, mon - 1, day)).getTime();
					return date;
				}
			}
		}

		return getDayStart(now.getTime());
	}

	function getTemplate(id) {
		var node = findId(id);
		if (!node) { throw new Error("Cannot get template: " + id); }
		var code = node.innerHTML.replace(/^\s*<!--/, '').replace(/-->\s*$/, '');
		return code;
	}
}


function showSupportButton() {
	var button = findId('js-manager-online');
	if (button) {
		button.style.display = 'block';
	}
}

function hotKeysManager() {
	var me = arguments.callee;
	if (me.inited) {		
		l("Double HK manager initialization");
		return false;
	}
	
	me.inited = true;
	var handlers = {}, handlersCount = 0, listenerHandle;
	var self = {
		add: add,
		remove: remove,
		MOD_CTRL: 1,
		MOD_ALT: 2,
		MOD_SHIFT: 4,
		KEY_LEFT: 37,
		KEY_RIGHT: 39
	};
	
	return self;
	
	function add(key, mod, fn) {
		var k = key + '_' + mod;
		if (!handlers[k]) {
			handlersCount ++;
		}
		
		handlers[k] = fn;
		if (!listenerHandle) {
			listenerHandle = addEvent(document.documentElement, 'keydown', onKeyDown);
		}
	}
	
	function remove(key, mod, fn) {
		var k = key + '_' + mod;
		if (!handlers[k]) {
			handlersCount ++;
		}
		delete handlers[k];
		
		if (handlersCount == 0 && listenerHandle) {
			removeEvent(listenerHandle);		
			listenerHandle = null;
		}
	}
	
	function onKeyDown(ev) {
		var e = ev || window.event;
		var t = e.target || e.srcElement;
		
		if (t.nodeName == 'INPUT' || t.nodeName == 'TEXTAREA') {
			return;
		}
		
		var mods = (e.ctrlKey ? self.MOD_CTRL : 0) || (e.altKey ? self.MOD_ALT : 0) ||
			(e.shiftKey ? self.MOD_SHIFT : 0);
		var k = e.keyCode + '_' + mods; 
		if (handlers[k]) {
			//l("Keydown: " + k + " detected");
			preventEvent(e);
			handlers[k](e);
		}
	}		
}

function pageScroller() {
	var me = pageScroller;
	return me.instance ? me.instance : (me.instance = me.init());
}

pageScroller.init = function () {
	var scroller = null, doc = document.documentElement, body = document.body;
	
	function getScroller() {
		if (!scroller) {
			scroller = Animate(function (val) {
				doc.scrollTop ? (doc.scrollTop = val) : 
					body.scrollTop ? (body.scrollTop = val) : (body.scrollTop = doc.scrollTop = val);
			}, { from: 0, to: 999999 });			
		}
		
		return scroller;
	}
	
	function scrollTo(val, time) {
		getScroller();
		// WebKit 5 years lasting bug
		var scrollPos = document.documentElement.scrollTop || document.body.scrollTop;
		scroller.stop();
		scroller.set(scrollPos);
		scroller.goTo(val, time || 350);
	}
	
	function scrollToElement(elm, offset, time) {
		var pos = getPosition(elm);
		scrollTo(pos.y + offset || 0, time);
	}
	
	return { scrollTo: scrollTo, scrollToElement: scrollToElement };
}

function resizeListener() {
	var me = arguments.callee;
	return me.instance ? me.instance : (me.instance = me.init());
}

resizeListener.init = function() {
	var handler = null, oldHandler = null;
	var width = document.documentElement.clientWidth;
	var callbacks = {}, id = 0, count = 0, lastCall = null, to = null, interval = 500;
	
	function add(cbk) {
		if (!handler) {
			
			// Setup listener
			oldHandler = window.onresize;
			window.onresize = handler = wrap(function(ev) {
				if (width != document.documentElement.clientWidth) {
					width = document.documentElement.clientWidth;
					throttleRunCallbacks();					
					oldHandler && oldHandler.call(null, ev);										
				}
			}, 'resize_listener');
		}
		
		id++;count++;
		callbacks[id] = cbk;
		return id;
	}
	
	function throttleRunCallbacks() {
		var d = (new Date()).getTime();
		if (d > lastCall + interval) {
			to && clearTimeout(to);
			to = null;
			lastCall = d;
			runCallbacks();
		} else {
			if (!to) {
				to = setTimeout(throttleRunCallbacks, interval);
			}
		}
	}
	
	function runCallbacks() {
		for (var i in callbacks){
			callbacks[i]();
		}
	}
	
	return { add: add };
}

wrap(function() {
	// Initialization
	var managers = findId('block-managers');
	managers && ieFixScroller(managers);
	
	managers && attachScroller(managers);		
	
	managers && attachTooltip(managers, { popupClass: 'tooltip-frame' });
	
	var imagesScroller = findId('images-scroller'), thumbScroller = null;
	imagesScroller && (thumbScroller = attachScroller(imagesScroller));
	
	imagesScroller && ieFixScroller(imagesScroller);
	
	var catalog = findId('catalog-frame');
	if (catalog) {
		var catalogCtrl = attachAjaxCatalog(catalog, window.jsConfig.catalogFilters);
	}	
	
	setupPlaceholders();	
	showSupportButton();
	
	var slideshowDiv = findId('images-slideshow');
	if (slideshowDiv) {
		var slideshow = attachSlideshow(slideshowDiv, { scroller: thumbScroller });		
	}
	
	if (catalogCtrl || slideshow) {
		var hkm = window.jsConfig.hkManager = hotKeysManager();
		
		if (catalogCtrl) {
			hkm.add(hkm.KEY_LEFT, hkm.MOD_CTRL, bind(catalogCtrl, catalogCtrl.prevPage));
			hkm.add(hkm.KEY_RIGHT, hkm.MOD_CTRL, bind(catalogCtrl, catalogCtrl.nextPage));
		} else if (slideshow) {
			hkm.add(hkm.KEY_LEFT, hkm.MOD_CTRL, bind(slideshow, slideshow.prevImage));
			hkm.add(hkm.KEY_RIGHT, hkm.MOD_CTRL, bind(slideshow, slideshow.nextImage));
		}
	}
	
	var inpArrive = findId('id-o-arrive');
	var inpDepart = findId('id-o-depart');
	var now = (new Date()).getTime();
	inpArrive && attachCalendar(inpArrive, { minDate: now });
	inpDepart && attachCalendar(inpDepart, { minDate: now });
	ieFixJumpingFloats();

} , 'script_init')();

// Support function
function openSupportWindow() {
    var d = new Date();
    d.setDate(d.getDate() + 7);
    document.cookie = "_MSG_=1;path=/;expires=" + d.toGMTString();
    d = window.screen;
    open("http://talkdriver.ru/support.php?sid=1438&crc=cd34a819&nf", "msg1438", "height=500,width=650,top="+(d ? (d.height-600)>>1 : 100)+",left="+(d ? (d.width-650)>>1 : 120)+",resizable=1");
    return false
}

function ieFixJumpingFloats() {
	if (isIE67) {
		setTimeout(function() { 
			document.body.appendChild(newNode('div', {cssText: 'position: absolute; left: -1000px; top: -100px;'}));
		}, 600);
	}
}

// IE fix
function ieFixScroller(root) {
	
	if (!root || !navigator.userAgent.match(/msie\s[67]\./i)) {
		return;
	}
	
	var 
		width = 10,
		inner = find('scroller-inner', root, 'div'),
		list = find('ie-fix-list', inner, 'ul'),
		children = list.childNodes;

	for (var i = children.length - 1; i>= 0; i--) {		
		width += children[i].offsetWidth;		
	}
	
	inner.style.width = width + 'px';
} 

