new function () {

	var YMaps = window.YMaps, $ = YMaps.jQuery;

	var YandexMaps = window.YandexMaps = function (mapSelector) {
		if (typeof mapSelector != 'string') throw new TypeError('mapPath should be string, ' + typeof mapSelector + ' now');

		this.map = new YMaps.Map($(mapSelector)[0]);
	};

	YandexMaps.prototype = {
		self: YandexMaps,
		point: function(x, y) {
			return new YMaps.GeoPoint(x,y);
		},
		objectsZoomTiles: null,
		setObjectsZoomTiles: function (objectsZoomTiles) {
			this.objectsZoomTiles = objectsZoomTiles;
			return this;
		},
		objectsTiles: null,
		setObjectsTiles: function (objectsTiles) {
			this.objectsTiles = this.wrapTiles(objectsTiles);
			return this;
		},
		wrapTiles: function (tiles) {
			var newTiles = {};
			for (var i in tiles) {
				newTiles[i] = Array.associate(tiles[i], ['lon1', 'lat1', 'lon2', 'lat2']);
				newTiles[i].tile   = i;
				newTiles[i].loaded = false;
			}
			return newTiles;
		},

		openLast: function () {
			if (this.lastMarker) {
				this.lastMarker.placemark.openBalloon();
			}
			return this;
		},
		initClusterer: function (size, nonBlocking) {
			this.clusterer = new Clusterer(this.map, size).autoUpdate(nonBlocking);
			return this;
		},
		autoUpdate: function () {
			setTimeout(this.update.bind(this), 100);
			YMaps.Events.observe(this.map, this.map.Events.BoundsChange, this.update, this);
		},
		setCenter: function (x, y, zoom) {
			this.map.setCenter(this.point(x, y), zoom);
			return this;
		},
		getBounds: function () {
			return this.getClosestBounds() || this.getWideBounds();
		},
		getClosestBounds: function () {
			var bounds = this.map.getBounds();
			var zoomTiles = this.objectsZoomTiles[this.map.getZoom()];
			if (!zoomTiles || !zoomTiles.length) return false;
			var tiles = this.objectsTiles, i = zoomTiles.length;
			var lonMin = bounds.getLeft(), latMin = bounds.getBottom();
			while (i--) {
				var tile = tiles[zoomTiles[i]];
				if (tile.lon1 < lonMin && tile.lat1 < latMin) {
					return tile;
				}
			}
			return false;
		},
		getUrl: function () {
			return this.ajaxLoadObjectsHost + this.ajaxLoadObjectsUrl;
		},
		// padding in percent (0.5)
		padding: 0.5,
		getPadPix: function (bounds) {
			return {
				v: (bounds.getTop() - bounds.getBottom()) * this.padding,
				h: (bounds.getRight() - bounds.getLeft()) * this.padding
			};
		},
		getWideBounds: function () {
			var bounds = this.map.getBounds(),
				padPix = this.getPadPix(bounds);
			return {
				lon1: bounds.getLeft()   - padPix.h,
				lon2: bounds.getRight()  + padPix.h,
				lat1: bounds.getBottom() - padPix.v,
				lat2: bounds.getTop()    + padPix.v
			};
		},
		update: function () {
			return this.load(this.getBounds());
		},
		load: function (bounds) {
			if (!bounds.loaded) bounds.tile ?
				  this.loadByNumber(bounds.tile)
				: this.loadByCoord(bounds);
			return this;
		},
		loadByCoord: function (coords) {
			this.jsonp(this.getUrl() + '?' + $.param(coords));
			return this;
		},
		loadByNumber: function (num) {
			this.jsonp(this.getUrl() + num + '/');
			return this;
		},
		jsonp: function (url) {
			return $.ajax({
				url: url,
				cache: this.ajaxCache,
				jsonp: 'callback',
				jsonpCallback: 'jsonp_map',
                                dataType: 'jsonp',
				success: this.loadObjects.bind(this)
			});
		},
		lastMarker: null,
		loadObjects: function (result) {
			var newMarkersCount = 0;
			for (var i = result.length; i--;) if (!Marker.factory.exists(result[i])) {
				var marker = Marker.factory.produce(this.map, result[i]);
				this.clusterer ?
					this.clusterer.addMarker(marker) :
					this.map.addOverlay(marker.placemark);
				newMarkersCount++;
				this.lastMarker = marker;
			}
			if (newMarkersCount && this.clusterer) this.clusterer.update();
			return this;
		}
	};
}();

