function shoutbox(optionArr){
	if(!shoutbox.instance){
		shoutbox.instance = this;
	}else if(shoutbox.instance.properties.singleton){
		return shoutbox.instance;
	}
	

	this.properties = {
		id: 'shoutbox',
		singleton: 0,
		url: 'ajax_backend.php',
		title: 'Shoutbox',
		button_text: 'Shout',
		onInit: function(){},
		show_count: 5,
		msg_height: 65,
		highlight_color: '#F9FF99',
		msg_max_length: 150
	}
	
	this.messages = {};
	this.new_messages_id = [];
	this.count_all = this.count = this.all_loaded = 0;
	this.showing = [];

	var keys = [];
	var bad_words = 0;
	var busy = 0;
	var eof = 0;
	var bof = 1;
	var send_status = 0;

	var _self = this;

	
	this.init = function(options){
		$.extend(_self.properties, options);
		
		$('#'+_self.properties.id)
			.addClass('shoutbox')
			.append('<h2 class="index_left">'+_self.properties.title+'<span class="shoutbox_new_count float_right"></span></h2>')
			.append('<div class="shoutbox-prev shoutbox-prev-disabled"></div>')
			.append('<div class="shoutbox_wrapper" style="height: '+((_self.properties.msg_height+10)*_self.properties.show_count)+'px"><div class="shoutbox_scroller"></div></div>')
			.append('<div class="shoutbox-next shoutbox-next-disabled"></div>')
			.append('<div><textarea id="'+_self.properties.id+'_shout_text" name="shout_text" rows="3"></textarea><div id="bad_div_'+_self.properties.id+'_shout_text" class="bad_div_left"></div></div><div><input type="button" value="'+_self.properties.button_text+'" /><span id="'+_self.properties.id+'_msg_count" class="float_right">'+_self.properties.msg_max_length+'</span></div>')
			.on('click', '.shoutbox_user_name', function(){
				$('#'+_self.properties.id+'_shout_text').val($('#'+_self.properties.id+'_shout_text').val() + $(this).text() + ', ');
			});


		$('body').append('<div id="'+_self.properties.id+'_full_text" class="shoutbox_full_text" style="display:none;"><div>')
			.on('mouseleave', '#'+_self.properties.id+'_full_text', function(){$('#'+_self.properties.id+'_full_text').fadeOut(0);});
		$('#'+_self.properties.id+'_shout_text')
			.keyup(function(){bad_words = BadWords(_self.properties.id+'_shout_text');})
			.on('keyup blur', function(){
				var msg_length = $(this).val().length;
				if (msg_length > _self.properties.msg_max_length) {
					$(this).val($(this).val().substring(0, _self.properties.msg_max_length)).scrollTop(1000);
				}
				msg_length = $(this).val().length;
				$('#'+_self.properties.id+'_msg_count').text(_self.properties.msg_max_length - msg_length);
			});
		$('#'+_self.properties.id).find('input[type="button"]').click(function(){if(!bad_words) _self.sendMessage();});

		$('#'+_self.properties.id).on('click', '.shoutbox-next-enabled', function(){_self.scrollNext();});
		$('#'+_self.properties.id).on('click', '.shoutbox-prev-enabled', function(){_self.scrollPrev();});
		$.ajax({
			type: 'POST',
			url: _self.properties.url,
			data: {
				sel: 'shoutbox', 
				data: {'shoutbox_last': {'from_id': 0}}
			},
			success: function(resp){
				if(resp.shoutbox_last){
					if(resp.shoutbox_last.count == resp.shoutbox_last.count_all){
						_self.all_loaded = 1;
					}
					_self.addMessages(resp.shoutbox_last.data);
					_self.setShowMessages(_self.getLastId());
					_self.showMessages();
				}else{
					_self.all_loaded = 1;
				}
				_self.properties.onInit();
			},
			dataType: 'json',
			backend: 1
		});
	}

	
	this.sendMessage = function(){
		var msg = $('#'+_self.properties.id+'_shout_text').val();
		if(msg && !send_status){
			send_status = 1;
			$.ajax({
				type: 'POST',
				url: _self.properties.url,
				data: {
					sel: 'shoutbox', 
					data: {send_message: {text: msg, last_id: _self.getLastId()}}
				},
				success: function(resp){
					busy = 0;
					if(resp.send_message){
						_self.addMessages(resp.send_message.data, 1);
						_self.setShowMessages(_self.getLastId());
						_self.showMessages('new');
					}
					$('#'+_self.properties.id+'_shout_text').val('');
					send_status = 0;
				},
				error: function(){
					send_status = 0;
				},
				dataType: 'json',
				backend: 0
			});
		}
	}
	
	
	this.addMessages = function(msg, add_new){
		add_new = add_new || 0;
		for(var id in msg){
			if(msg.hasOwnProperty(id)){
				_self.messages[id] = msg[id];
				if(add_new){
					bof = 0;
					_self.updateScrollButtons();
					_self.new_messages_id.push(parseInt(id));
				}
			}
		}
		rebuildKeys();
		
		return this;
	}
	
	
	this.addNewMessages = function(msg){
		var show = bof ? true : false;
		_self.addMessages(msg.data, 1);
		if(show){
			_self.setShowMessages(_self.getLastId());
			_self.showMessages('new');
		}else{
			_self.updateUnreadCount();
		}
		
		return this;
	}
	
	
	this.setShowMessages = function(from_id, callback, up){
		callback = callback || function(){};
		up = up || 0;
		var count = _self.properties.show_count;
		if(up){
			var to_key = _self.messages[from_id].key;
			var from_key = to_key - count + 1;
			if(from_key < 0){
				from_key = 0;
				to_key = from_key + count - 1;
			}
		}else{
			var from_key = _self.messages[from_id].key;
			var to_key = from_key + count - 1;
			if(to_key >= keys.length && _self.all_loaded){
				to_key = keys.length-1;
				from_key = to_key - count + 1;
				if(from_key < 0){
					from_key = 0;
				}
			}
		}
		if(to_key >= keys.length){
			_self.loadMessages(function(){
				bof = (from_key == 0) ? 1 : 0;
				eof = (to_key == keys.length-1  && _self.all_loaded) ? 1 : 0;
				_self.setShowMessages(from_id, callback, up);
			});
			return;
		}else{
			_self.showing = [];
			for(var i=from_key; i<=to_key; i++){
				var id = keys[i];
				_self.showing.push(_self.messages[id]);
			}
		}
		
		//preload next messages
		if(!_self.all_loaded && to_key+count >= keys.length){
			/*busy = 1;
			$('#'+_self.properties.id).find('.shoutbox-next').removeClass('shoutbox-next-enabled').addClass('shoutbox-next-disabled');
			_self.loadMessages(function(){
				bof = (from_key == 0) ? 1 : 0;
				eof = (to_key == keys.length-1 && _self.all_loaded) ? 1 : 0;
				busy = 0;
				_self.updateScrollButtons();
			});*/
			if(callback){
				callback();
			}
		}else{
			if(callback){
				callback();
			}
		}
		bof = (from_key == 0) ? 1 : 0;
		eof = (to_key == keys.length-1 && _self.all_loaded) ? 1 : 0;
		_self.updateScrollButtons();
		
		return this;
	}
	
	
	this.updateScrollButtons = function(){
		if(eof){
			$('#'+_self.properties.id).find('.shoutbox-next').removeClass('shoutbox-next-enabled').addClass('shoutbox-next-disabled');
		}else{
			$('#'+_self.properties.id).find('.shoutbox-next').removeClass('shoutbox-next-disabled').addClass('shoutbox-next-enabled');
		}
		if(bof){
			$('#'+_self.properties.id).find('.shoutbox-prev').removeClass('shoutbox-prev-enabled').addClass('shoutbox-prev-disabled');
		}else{
			$('#'+_self.properties.id).find('.shoutbox-prev').removeClass('shoutbox-prev-disabled').addClass('shoutbox-prev-enabled');
		}
	}
	
	this.scrollNext = function(){
		if(keys.length && !busy){
			var from_key = (_self.showing.length) ? _self.showing[_self.showing.length-1].key + 1 : 0;
			var from_id = (typeof(keys[from_key])!=='undefined') ? keys[from_key] : keys[from_key-1];
			_self.setShowMessages(from_id, function(){_self.showMessages('next')});
		}
	}
	
	
	this.scrollPrev = function(){
		if(keys.length && !busy){
			var from_key = (_self.showing.length) ? _self.showing[0].key - 1 : 0;
			var from_id = (typeof(keys[from_key])!=='undefined') ? keys[from_key] : keys[from_key+1];
			_self.setShowMessages(from_id, function(){_self.showMessages('prev')}, 1);
		}
	}
	
	
	this.loadMessages = function(callback){
		callback = callback || function(){};
		if(!_self.all_loaded){
			busy = 1;
			var from_id = keys.length ? keys[keys.length-1] : 0;
			$.ajax({
				type: 'POST',
				url: _self.properties.url,
				data: {
					sel: 'shoutbox', 
					data: {'load_messages': {'from_id': from_id}}
				},
				success: function(resp){
					busy = 0;
					if(resp.load_messages){
						if(resp.load_messages.count == resp.load_messages.count_all){
							_self.all_loaded = 1;
						}
						_self.addMessages(resp.load_messages.data);
					}else{
						_self.all_loaded = 1;
					}
					callback();
					_self.updateUnreadCount();
				},
				dataType: 'json',
				backend: 0
			});
		}
	}
	
	
	this.getLastId = function(){
		if(keys.length){
			return keys[0];
		}else{
			return 0;
		}
	}
	
	this.getFirstId = function(){
		if(keys.length){
			return keys[keys.length-1];
		}else{
			return 0;
		}
	}
	
	
	var rebuildKeys = function(){
		keys = [];
		for(var id in _self.messages){
			if(_self.messages.hasOwnProperty(id)){
				keys.push(parseInt(id));
			}
		}
		keys.sort(function(a,b){
			if(a>b) return -1;
			if(a<b) return 1;
			return 0;
		});
		var len = keys.length;
		for(var i=0; i<len; i++){
			_self.messages[keys[i]].key = i;
		}
		_self.count = len;
		return this;
	}
	
	
	/* GUI part*/
	this.showMessages = function(type){
		type = type || '';
		createMessagesBlock(type);
		_self.updateUnreadCount();
	}
	
	
	var createMessagesBlock = function(type){
		if(!_self.showing.length){
			busy = 0;
			return this;
		}
		busy = 1;
		var scroller = $('#'+_self.properties.id).find('.shoutbox_scroller');

		var len = _self.showing.length;
		var added_height = 0;
		if(type == 'prev' || type == 'new'){
			for(var i=len-1; i>=0; i--){
				var msg = _self.showing[i];
				if(!$('#'+_self.properties.id+'_shoutbox_msg_'+msg.id_msg).size()){
					var elem = $('<div id="'+_self.properties.id+'_shoutbox_msg_'+msg.id_msg+'" class="shoutbox_msg_cont" style="height: '+_self.properties.msg_height+'px"><div class="float_left"><a href="'+msg.profile_link+'"><img src="'+msg.icon+'" /></a></div> <div class="shoutbox_msg"><div class="shoutbox_msg_header"><span class="shoutbox_user_name">'+msg.name+'</span><span class="float_right">'+msg.date_str+'</span></div><div class="shoutbox_msg_body">'+msg.text+'</div></div></div>');
					$(elem).prependTo($(scroller));
					added_height += $(elem).outerHeight(true);
					setMessageElement(elem);
				}
			}
		}else{
			for(var i=0; i<len; i++){
				var msg = _self.showing[i];
				if(!$('#'+_self.properties.id+'_shoutbox_msg_'+msg.id_msg).size()){
					var elem = $('<div id="'+_self.properties.id+'_shoutbox_msg_'+msg.id_msg+'" class="shoutbox_msg_cont" style="height: '+_self.properties.msg_height+'px"><div class="float_left"><a href="'+msg.profile_link+'"><img src="'+msg.icon+'" /></a></div> <div class="shoutbox_msg"><div class="shoutbox_msg_header"><span class="shoutbox_user_name">'+msg.name+'</span><span class="float_right">'+msg.date_str+'</span></div><div class="shoutbox_msg_body">'+msg.text+'</div></div></div>');
					$(elem).appendTo($(scroller));
					added_height += $(elem).outerHeight(true);
					setMessageElement(elem);
				}
			}
		}

		if(type == 'prev'){
			$(scroller).css('top', '-'+added_height+'px').animate({'top': '+='+added_height+'px'}, function(){
				if($(scroller).find('.shoutbox_msg_cont').size() > _self.properties.show_count){
					var len = $(scroller).find('.shoutbox_msg_cont').size() - _self.properties.show_count;
					for(var i=0; i<len; i++){
						$(this).find('.shoutbox_msg_cont:last').remove();
					}
				}
				$(this).css('top', '0');
				_self.blinkNewMessages();
				_self.updateUnreadCount();
				busy = 0;
			});
		}else if(type == 'next'){
			$(scroller).animate({'top': '-='+added_height+'px'}, function(){
				if($(scroller).find('.shoutbox_msg_cont').size() > _self.properties.show_count){
					var len = $(scroller).find('.shoutbox_msg_cont').size() - _self.properties.show_count;
					for(var i=0; i<len; i++){
						$(this).find('.shoutbox_msg_cont:first').remove();
					}
				}
				$(this).css('top', '0');
				_self.blinkNewMessages();
				_self.updateUnreadCount();
				busy = 0;
			});
		}else if(type == 'new'){
			if($(scroller).find('.shoutbox_msg_cont').size() > _self.properties.show_count){
				var len = $(scroller).find('.shoutbox_msg_cont').size() - _self.properties.show_count;
				for(var i=0; i<len; i++){
					$(scroller).find('.shoutbox_msg_cont:last').remove();
				}
			}
			_self.blinkNewMessages();
			_self.updateUnreadCount();
			busy = 0;
		}else{
			busy = 0;
		}
		
	}
	
	
	var setMessageElement = function(elem){
		if($(elem).find('.shoutbox_msg').height() > $(elem).height()){
			$(elem).append('<div class="shoutbox_dots">...</div>');
			$(elem).on('mouseenter', '.shoutbox_msg_body', function(){
				var top = $(this).offset().top-1;
				var left = $(this).offset().left-1;
				var width = $(this).width()+2;
				$('#'+_self.properties.id+'_full_text').html($(this).html()).css({'top': top+'px', 'left': left+'px', width: width+'px'}).fadeIn(0);
			});
		}
	}
	
	
	this.blinkNewMessages = function(){
		if(_self.new_messages_id.length){
			var new_messages_id_tmp = [];
			var len = _self.new_messages_id.length;
			for(var i=0; i<len; i++){
				var msg_id = _self.new_messages_id[i];
				if($('#'+_self.properties.id+'_shoutbox_msg_'+msg_id).size()){
					var old_bg = $('#'+_self.properties.id+'_shoutbox_msg_'+msg_id).css('background-color');
					$('#'+_self.properties.id+'_shoutbox_msg_'+msg_id+', #'+_self.properties.id+'_shoutbox_msg_'+msg_id+' .shoutbox_dots').animate(
						{'background-color': _self.properties.highlight_color},
						1000,
						function(){
							$(this, this+' .shoutbox_dots').animate(
								{'background-color': old_bg},
								1000,
								function(){
									$(this).css('background-color', old_bg);
								}
							);
						}
					);
				}else{
					new_messages_id_tmp.push(msg_id);
				}
			}
			_self.new_messages_id = [];
			$.extend(_self.new_messages_id, new_messages_id_tmp);
		}
		return this;
	}
	
	this.updateUnreadCount = function(){
		var html = _self.new_messages_id.length ? '('+_self.new_messages_id.length+')' : '';
		$('#'+_self.properties.id).find('.shoutbox_new_count').html(html);
		return this;
	}
	/* --- */

	_self.init(optionArr);
};