function Clusterer (map, size) {
	this.map  = map;
	this.conv = map.converter;
	this.size = size || 60;
	this.markers = [];
	this.cluster = null;
	this.stop = function () {};

	this.bounds = {
		top   : -9999999,
		right : -9999999,
		bottom: 99999999,
		left  : 99999999
	};
}

Clusterer.prototype = {
	addMarker: function (marker) {
		var b = this.bounds, c = marker.coord, x = c.getX(), y = c.getY();
		this.markers.push(marker.add().hide());
		b.top    = Math.max(b.top   , y);
		b.bottom = Math.min(b.bottom, y);
		b.right  = Math.max(b.right , x);
		b.left   = Math.min(b.left  , x);
		return this;
	},
	removeMarker: function (marker) {
		Array.erase(this.markers, marker);
		return this;
	},
	pixelSize: function () {
		var b = this.map.getBounds();
		var c = this.map.getContainerSize();
		return {
			x: Math.abs(b.getRight() - b.getLeft()) / c.y,
			y: Math.abs(b.getTop() - b.getBottom()) / c.x
		};
	},
	blockSize: function () {
		var pixel = this.pixelSize(), block = this.size;
		return { x: pixel.x * block, y: pixel.y * block };
	},
	activeMapSize: function () {
		var b = this.bounds;
		return {
			w: (b.right-b.left),
			h: (b.top-b.bottom)
		};
	},
	clusterLength: function () {
		var m = this.activeMapSize(), s = this.blockSize();
		return {
			x: Math.ceil(m.w / s.x) || 1,
			y: Math.ceil(m.h / s.y) || 1
		};
	},
	releaseCluster: function () {
		var length  = this.clusterLength();
		var cluster = this.cluster = new Array(length.y);
		for (var y = length.y; y--;) {
			cluster[y] = new Array(length.x);
			for (var x = length.x; x--;) cluster[y][x] = {};
		}
		return this;
	},
	blockShift: function () {
		var length = this.clusterLength();
		var b = this.bounds;
		return {
			x:  b.left,
			w: (b.right - b.left) / length.x,
			y:  b.bottom,
			h: (b.top - b.bottom) / length.y
		};
	},
	addToCluster : function (mark, shift) {
		var cluster = this.cluster;
		var f = Math.floor,
		    y = f((mark.coord.getY() - shift.y) / shift.h) || 0,
		    x = f((mark.coord.getX() - shift.x) / shift.w) || 0;
		if (y && cluster   .length == y) y--;
		if (x && cluster[y].length == x) x--;
		var cell = cluster[y][x];
		if (! (mark.type in cell)) cell[mark.type] = [];
		cell[mark.type].push(mark);
		return cell;
	},
	showClusterCallback : function () {
		this.stop = Array.hardEachBack(this.cluster,
			function (row) {
				for (var x = 0, l = row.length; x < l; x++) {
					for (var i in row[x]) row[x][i][0].show();
				}
			});
		return this;
	},
	showCluster: function () {
		var cluster = this.cluster;
		if (!cluster) return this;
		for (var y = cluster.length; y--;) for (var x = 0, l = cluster[y].length; x < l; x++) {
			var cell = cluster[y][x];
			for (var i in cell) cell[i][0].show();
		}
		return this;
	},
	recountCallback: function (fn, progress) {
		var cont    = $(this.map.getContainer()).addClass('hide-YMaps-placemarks');
		var markers = this.markers;
		var shift   = this.blockShift();
		var callPr  = $.isFunction(progress);
		var i = 0;
		this.stop = Array.hardLoop({
			// length of array can changed
			cond: function () { return i < markers.length; },
			fn: function () {
				var stop = Math.min(i + 100, markers.length);
				for (;i < stop; i++) {
					var mark = markers[i].hide();
					this.addToCluster(mark, shift);
				}
				callPr && progress(i/markers.length*100);
			}.bind(this),
			ready: function () {
				cont.removeClass('hide-YMaps-placemarks');
				fn()
			}
		});
	},
	recount: function (fn, progress) {
		if (!this.markers.length) {
			if ($.isFunction(fn)) fn();
			return this;
		}

		this.releaseCluster();
		if (fn) return this.recountCallback(fn, progress);

		var markers = this.markers;
		var shift   = this.blockShift();
		for (var i = 0, l = markers.length; i < l; i++) {
			var mark = markers[i].hide();
			this.addToCluster(mark, shift);
		}
		
		return this;
	},
	autoUpdate: function (fn, progress) {
		YMaps.Events.observe(this.map, this.map.Events.Update, this.update, this);
		return this;
	},
	update: function () {
		return this.updateCfg().update();
	},
	updateCfg: function (fn, progress) {
		this.update = function () {
			this.stop();
			return (!fn) ? this.recount().showCluster() :
				this.recount(function () {
					this.showCluster();
					if (typeof fn == 'function') fn();
				}.bind(this), progress);
		}.bind(this);
		return this;
	}
};
