// ClusterManager
TRAILS.utils.ClusterManager = function(map, markers, gridSize){
	var self = this;

	this.map = map;
	this._moveend = null;
	this._gridSize = typeof gridSize === Number.type && gridSize ? gridSize : 60;
	this._clusters = new Array();
	this._invisibleMarkers = new Array();
	this._eventManager = new TRAILS.utils.EventManager();
	this._moveend = google.maps.Event.addListener(map, 'moveend', function(){ self.reset(); });

	if(typeof markers === Object.type && markers.length)
		this.addMarkers(markers);

	this._eventManager.trigger('load');
};
TRAILS.utils.ClusterManager.prototype.isVisibleMarker = function(marker){
	return this.map.getBounds().containsLatLng(marker.getLatLng());
};
TRAILS.utils.ClusterManager.prototype._addMarkers = function(markers){	
	for(var i = 0, l = markers.length, cluster = new Array(); i < l; i++)
		this._addMarker(markers[i].marker, true, true, cluster);

	if(this._invisibleMarkers.length === 0)
		return;

	for(var i = 0, l = this._invisibleMarkers.length; i < l; i++)
		this._addMarker(this._invisibleMarkers[i], true, true);

	this._invisibleMarkers = new Array();
};
TRAILS.utils.ClusterManager.prototype.getVisibleClusters = function(){
	var clusters = new Array();
	
	for(var i = 0, l = this._clusters.length, bounds = this.map.getBounds(); i < l; i++){
		var cluster = this._clusters[i];
		
		if(cluster.isVisible(bounds))
			clusters.push(cluster);
	}

	return clusters;
};
TRAILS.utils.ClusterManager.prototype._addMarker = function(marker, disableRedraw, disableVisibilityCheck, clusters){
	if(!disableVisibilityCheck && !this.isVisibleMarker(marker))
		return this._invisibleMarkers.push(marker);
	if(typeof clusters !== Object.type || clusters === null)
		clusters = this._clusters;
	
	for(var i = 0, l = clusters.length, d = this.map.fromLatLngToDivPixel(marker.getLatLng()); i < l; i++){
		var cluster = clusters[i];
		var center = cluster.getCenter();
	
		if(center === null)
			continue;
	
		var divPixel = this.map.fromLatLngToDivPixel(center);

		// found a cluster that contains this marker
		if(d.x >= divPixel.x - this._gridSize && d.x <= divPixel.x + this._gridSize && d.y >= divPixel.y - this._gridSize && d.y <= divPixel.y + this._gridSize){
			cluster.addMarker({ isAdded: false, marker: marker });
	
			if(!disableRedraw)
				cluster.redraw();
			
			return;
		}
	}

	// since no cluster contains this marker create a new cluster with this marker
	var cluster = new TRAILS.utils.ClusterManager.Cluster(this);

	cluster.addMarker({ isAdded: false, marker: marker });
	
	if(!disableRedraw)
		cluster.redraw();

	clusters.push(cluster);
	
	if(clusters !== this._clusters)
		this._clusters.push(cluster);
};
TRAILS.utils.ClusterManager.prototype.reset = function(){
	var clusters = this.getVisibleClusters();
	var markers = new Array();

	for(var i = 0, l = clusters.length; i < l; ++i){
		var cluster = clusters[i];
		var zoom = cluster.getZoom();
		
		// if the zoom has changed since this cluster was created then destroy the cluster and collect its markers
		if(zoom !== null && this.map.getZoom() !== zoom){			
			for(var j = 0, m = cluster.getMarkers(), n = m.length; j < n; ++j)					
				markers.push({ isAdded: false, marker: m[j].marker });
			
			cluster.clearMarkers();
			
			for(var j = 0, n = this._clusters.length; j < n; ++j)
				if(cluster === this._clusters[j])
					this._clusters.splice(j, 1);
		}
	}

	this._addMarkers(markers);
	this.redraw();
};
TRAILS.utils.ClusterManager.prototype.clearMarkers = function(){
	this._eventManager.trigger('clearMarkers');
	google.maps.Event.removeListener(this._moveend);
	
	for(var i = 0, l = this._clusters.length; i < l; ++i)
		this._clusters[i].clearMarkers();
			
	this._clusters = new Array();
	this._invisibleMarkers = new Array();
	
	this._eventManager.trigger('clearMarkersEnd');
};
TRAILS.utils.ClusterManager.prototype.removeMarker = function(marker){
	this._eventManager.trigger('remove');
	
	for(var i = 0, l = this._clusters.length; i < l; i++){
		var cluster = this._clusters[i];
		
		if(cluster.remove(marker))
			return cluster.redraw();
	}
			
	this._eventManager.trigger('removeEnd');
};
TRAILS.utils.ClusterManager.prototype.redraw = function(){
	this._eventManager.trigger('redraw');
	
	for(var i = 0, clusters = this.getVisibleClusters(), l = clusters.length; i < l; ++i)
		clusters[i].redraw(true);
		
	this._eventManager.trigger('redrawEnd');
};
TRAILS.utils.ClusterManager.prototype.addMarkers = function(markers){
	this._eventManager.trigger('addMarkers');
	
	for(var i = 0, l = markers.length; i < l; i++)
		this._addMarker(markers[i], true);

	this._eventManager.trigger('addMarkersEnd');

	this.redraw();
};
TRAILS.utils.ClusterManager.prototype.addListener = function(eventName, fn){
	this._eventManager.bind(eventName, fn);
};
TRAILS.utils.ClusterManager.prototype.getGridSize = function(){
	return this._gridSize;
};
TRAILS.utils.ClusterManager.prototype.getMarkerCount = function(){
	var result = 0;
	
	for(var i = 0, l = this._clusters.length; i < l; i++)
		result += this._clusters[i].getMarkerCount();
		
	return result;
};
TRAILS.utils.ClusterManager.prototype.getTotalClusters = function(){
	return this._clusters.length;
};
// ClusterManger.Cluster
TRAILS.utils.ClusterManager.Cluster = function(clusterManager){
	this.map = clusterManager.map;
	this.manager = clusterManager;
	this._center = null;
	this._marker = null;
	this._markers = new Array();
	this._zoom = this.map.getZoom();
	this._bounds = new google.maps.LatLngBounds();
};
TRAILS.utils.ClusterManager.Cluster.prototype.getMarkers = function(){
	return this._markers;
};
TRAILS.utils.ClusterManager.Cluster.prototype.getCenter = function(){
	return this._center;
};
TRAILS.utils.ClusterManager.Cluster.prototype.getZoom = function(){
	return this._zoom;
};
TRAILS.utils.ClusterManager.Cluster.prototype.getMarkerCount = function(){
	return this._markers.length;
};
TRAILS.utils.ClusterManager.Cluster.prototype.isVisible = function(bounds){
	// this is where the magic happens. the faster this method runs, the better the user experience.
	if(this._center === null)
		return false;
	if(!bounds)
		bounds = this.map.getBounds();

	var sw = this.map.fromLatLngToDivPixel(bounds.getSouthWest());
	var ne = this.map.fromLatLngToDivPixel(bounds.getNorthEast());
	var centerxy = this.map.fromLatLngToDivPixel(this._center);
	var gridSize = this.manager.getGridSize();
	var zoom = this.map.getZoom();
	
	if(this._zoom !== zoom)
		gridSize = Math.pow(2, zoom - this._zoom) * gridSize;
	if(ne.x !== sw.x && (centerxy.x + gridSize < sw.x || centerxy.x - gridSize > ne.x))
		return false;
	if(centerxy.y + gridSize < ne.y || centerxy.y - gridSize > sw.y)
		return false;

	return true;
};
TRAILS.utils.ClusterManager.Cluster.prototype.addMarker = function(marker){
	var latLng = marker.marker.getLatLng();
	
	if (this._center === null)
		this._center = latLng;
		
	this._bounds.extend(latLng);
	this._markers.push(marker);
};
TRAILS.utils.ClusterManager.Cluster.prototype.removeMarker = function(marker){
	var isRemoved = false;
	
	this._bounds = new LatLngBounds();
	
	for(var i = 0, l = this._markers.length; i < l; i++){
		var thisMarker = this._markers[i].marker;
		
		if(marker === thisMarker){
			if(thisMarker.isAdded)
				this.map.removeOverlay(thisMarker);
				
			this._markers.splice(i, 1);
			
			isRemoved = true;
		}
		else
			this._bounds.extend(thisMarker);
	}

	return isRemoved;
};
TRAILS.utils.ClusterManager.Cluster.prototype.clearMarkers = function(){
	if (this._marker !== null)
		this.map.removeOverlay(this._marker);

	for(var i = 0, l = this._markers.length; i < l; i++){
		var marker = this._markers[i];
		
		if(marker.isAdded)
			this.map.removeOverlay(marker.marker);
	}
			
	this._markers = new Array();
};
TRAILS.utils.ClusterManager.Cluster.prototype.getMarkerStyle = function(markers){
	var markerStyle = 0;
	
	for(var a = [25, 50, 100, 200, 400], l = a.length, n = markers.length; markerStyle < l; markerStyle++)
		if(n < a[markerStyle])
			return markerStyle;
			
	return markerStyle;
};
TRAILS.utils.ClusterManager.Cluster.prototype.getMarkerText = function(markers){		
	return markers.length.toString();
};
TRAILS.utils.ClusterManager.Cluster.prototype.redraw = function(force){
	if(!force && !this.isVisible())
		return;
	// if the cluster has only one marker or the map is too zoomed then show the markers in this cluster
	if(this._markers.length === 1 || this.map.getZoom() > 13){
		for(var i = 0, l = this._markers.length; i < l; i++){
			var marker = this._markers[i];
			
			if (marker.isAdded)
				marker.marker.show();
			else {
				this.map.addOverlay(marker.marker);
				
				marker.isAdded = true;
			}
		}
		
		if(this._marker !== null)
			this._marker.hide();
	}
	// else just add a cluster marker to the map
	else{
		for(var i = 0, l = this._markers.length; i < l; i++){
			var marker = this._markers[i];
			
			if(marker.isAdded)
				marker.marker.hide();
		}
		
		if(this._marker === null){
			this._marker = new TRAILS.utils.ClusterManager.Marker(this._center, this._bounds, this.getMarkerText(this._markers), this.manager.getGridSize(), this.getMarkerStyle(this._markers));

			this.map.addOverlay(this._marker);
		}
		else{
			if(this._marker.isHidden())
				this._marker.show();
		
			this._marker.redraw(true);
		}
	}
};
// ClusterManger.Marker : google.maps.Overlay
TRAILS.utils.ClusterManager.Marker = function(latlng, bounds, text, padding, selectedStyle){	
	if(!this.styles[selectedStyle])
		selectedStyle = this.styles.length - 1;

	this._bounds = bounds;
	this._selectedStyle = selectedStyle;
	this._image = this.styles[selectedStyle].image;
	this._height = this.styles[selectedStyle].height;
	this._width = this.styles[selectedStyle].width;
	this._textColor = this.styles[selectedStyle].textColor;
	this._font = this.styles[selectedStyle].font;
	this._latlng = latlng;
	this._text = text || String.empty;
	this._padding = padding;
};
TRAILS.utils.ClusterManager.Marker.prototype = new google.maps.Overlay();
TRAILS.utils.ClusterManager.Marker.prototype.initialize = function(map){
	var self = this;
	var hasTransparencyIssues = jQuery.browser.msie6 && this._image.indexOf('.png')

	this.map = map;
	this._$container = jQuery('<div></div>')
		.appendTo(map.getPane(google.maps.MAP_MAP_PANE))
		.html(this._text)
		.css({
			height: this._height + 'px',
			width: this._width + 'px',
			textAlign: 'center',
			color: this._textColor,
			cursor: 'pointer',
			position: 'absolute',
			font: this._font,
			lineHeight: this._height + 'px',
			background: hasTransparencyIssues ? 'none' : 'url(' + this._image + ') no-repeat center center'
		})
		.disableTextSelect()
		.bind('click', function(event){
			var currentZoom = self.map.getZoom();
			var boundsZoom = self.map.getBoundsZoomLevel(self._bounds) - 1;
			
			while(boundsZoom <= currentZoom)
				boundsZoom++;
			
			self.map.setCenter(self._bounds.getCenter(), boundsZoom);
		});

	if(hasTransparencyIssues)
		this._$container[0].style.cssText += ';filter:progid:DXImageTransform.Microsoft.AlphaImageLoader' + '(src=\'' + this._image + '\', sizingMethod=\'scale\');';
};
TRAILS.utils.ClusterManager.Marker.prototype.remove = function(){
	this._$container.remove();
};
TRAILS.utils.ClusterManager.Marker.prototype.copy = function(){
	return new TRAILS.utils.ClusterManager.Marker(this._latlng, this._bounds, this._text, this._padding, this._selectedStyle);
};
TRAILS.utils.ClusterManager.Marker.prototype.redraw = function(force){
	if(!force) return;

	var divPixel = this.map.fromLatLngToDivPixel(this._latlng);

	divPixel.x -= (this._width / 2);
	divPixel.y -= (this._height / 2);

	this._$container.css({
		top: divPixel.y + 'px',
		left: divPixel.x + 'px'
	});
};
TRAILS.utils.ClusterManager.Marker.prototype.hide = function(){
	this._$container.hide();
};
TRAILS.utils.ClusterManager.Marker.prototype.show = function(){
	this._$container.show();
};
TRAILS.utils.ClusterManager.Marker.prototype.isHidden = function(){
	return this._$container.is(':hidden');
};
TRAILS.utils.ClusterManager.Marker.prototype.styles = (function(){
	var styles = new Array();
	var sizes = [53, 56, 66, 78, 90];

	for(var i = 0; i < 5; i++)
		styles.push({
			textColor: '#000',
			image: 'http://gmaps-utility-library.googlecode.com/svn/trunk/markerclusterer/images/m' + (i + 1) + '.png',
			height: sizes[i],
			width: sizes[i],
			font: 'bold 11px Arial,sans-serif'
		});
	
	return styles;
})();
