HM.GooglemapsClusters = Class.create({

  initialize: function(){
    this._setMap(arguments[0]);
     
    this.projectionObject      = new google.maps.OverlayView();
    this.projectionObject.draw = function() {};
    this.projectionObject.setMap(this.getMap());
        
    this.options = Object.extend({
      'gridsize'        : 100,
      'maxClusterZoom'  : 14,
      'styles'          : {
    		0   : {
  			  image     : 'http://gmaps-utility-library.googlecode.com/svn/trunk/markerclusterer/1.0/images/m1.png',
  			  textColor : '#FFFFFF',
  			  width     : 53,
  			  height    : 52
  		  },
    		10  : {
    			image     : 'http://gmaps-utility-library.googlecode.com/svn/trunk/markerclusterer/1.0/images/m2.png',
    			textColor : '#FFFFFF',
    			width     : 56,
    			height    : 55
    		},
    		20  : {
    			image     : 'http://gmaps-utility-library.googlecode.com/svn/trunk/markerclusterer/1.0/images/m3.png',
    			textColor : '#FFFFFF',
    			width     : 66,
    			height    : 65
    		}
    	}
    }, arguments[2] || {});
    
    this.setMarkers(arguments[1] || []);
    
    this._setGridsize(this.options.gridsize);
    this._setMaxZoom(this.options.maxZoom);
    
    this.element    = $(this.getMap().getDiv() || "map");
    this.clusters   = $A([]);
    this.prevZoom   = -1;
    this.zoomlevels = $A([]);
    
    google.maps.event.addListener(this.getMap(), 'zoom_changed', function() {
      if (this.timeout) window.clearTimeout(this.timeout);
      this.timeout = this.create.bind(this).delay(0.5);
    }.bind(this));

    google.maps.event.addListener(this.getMap(), 'dragend', function() {
      this.showClustersInBounds();
    }.bind(this));
    
  },
  
  create: function() {
    var zoom        = this.getMap().getZoom(),
        projection  = this.getProjection(),
        _clusters   = $A([]);
    
    // Cluster exists
    if (this.clusters[zoom]) {
      console.log("cache used for zoomlevel: ",zoom);
    
    // Create clusters
    }else{
      if (this.zoomlevels.indexOf(zoom) == -1) this.zoomlevels.push(zoom);
      this.getMarkers().each(function(m) {
        var pos = m.getPosition();
        
        // Find a cluster for this marker
        var cluster;  
        if ( zoom < this.getMaxZoom() && _clusters && (cluster = _clusters.find(function(_c) {
          if (_c.contains(pos)) return _c;
        }))){
          cluster.addMarker(m);
          
        // No cluster found create new one 
        }else{
          cluster = new HM.GooglemapsCluster(this,m);
          _clusters.push(cluster);
        } 
        
      }.bind(this));
      // Add cluster to zoom level 
      this.clusters[zoom] = _clusters; 
    }

    // Hide clusters of previous zoom level
    this.hideClusters();
    this.prevZoom = zoom;
    this.showClustersInBounds();
  },
  
  // Clusters
  deleteClusters: function() {
    if (!this.zoomlevels) return;
    this.zoomlevels.each(function(zoom) {
      this.hideClusters(zoom);
    }.bind(this));
    
    this.clusters = $A([]);
  },
  getClusters: function () {
    return this.clusters;
  },
  hideClusters: function() {
    var zoom = arguments[0] || this.prevZoom;
    if (this.clusters[zoom]){
      this.clusters[zoom].each(function(el) {
        el.hide();
      }.bind(this));
		}
  },
  showClustersInBounds: function() {
    var zoom   = arguments[0] || this.getMap().getZoom(),
        bounds = this.map.getBounds();
		if (this.clusters[zoom]) {
      this.clusters[zoom].each(function(el) {
        if (bounds.contains(el.getPosition())) {
          el.show();
        }
      });
    }
  },
  
  // Map
  getMap: function() {
    return this.map;
  },
  _setMap: function() {
    this.map = arguments[0];
  },
  
  // Markers
  addMarkers: function() {
    this.removeMarkers();
    this.getMarkers().push(arguments[0]);
    this._setMarkers();
  },
  getMarkers: function() {
    return this.markers;
  },
  removeMarkers: function() {
    if (!this.markers) return;
    this.markers = this.markers.map(function(el) {
      el.setMap(null);  
      return el;
    });    
  },
  setMarkers: function() {
    this.removeMarkers();
    this.markers = $A([]);
    this._setMarkers(arguments[0]);
  },
  _setMarkers: function() {
    var markers = $A(arguments[0]) || $A([]);
        markers.flatten();  
    this.markers = markers.findAll(function(el){
      if (el.getVisible()) return el;
    });
    this.deleteClusters();
  },
  
  // Maximum zoomlevel on which clustering will take place
  getMaxZoom: function() {
    return this.maxZoom;
  },
  _setMaxZoom: function() {
    this.maxZoom = arguments[0] || this.options.maxClusterZoom;
  },
  
  // GridSize
  getGridsize: function() {
    return this.gridsize;
  },
  _setGridsize: function() {
    this.gridsize = parseInt((arguments[0] || this.options.gridsize) / 2); 
  },
  
  // Projection
  getProjection: function() {
    return this._setProjection();
  },
  _setProjection: function() {
    return this.projectionObject.getProjection();
  },
  
  // Cluster images
  getStyles: function(){
    return this.options.styles;
  },
  
  nop: function() {}
  
});

HM.GooglemapsCluster = Class.create({

  initialize: function(){
    // Get properties from marker
    var param = arguments[0];
    
    var clusterManager = arguments[0],
        origin         = arguments[1],
  	    center         = origin.getPosition();
    
    this.map         = clusterManager.getMap();
  	this.gridsize    = clusterManager.getGridsize();
    this.styles      = clusterManager.getStyles();
  	this.projection  = clusterManager.getProjection();
    
    this.markers       = $A([]);
  	this.bounds        = null;
  	this.clusterMarker = null;
  	
    this._setCenter(center);
  	
    // Calculate bounds
  	var position = this.projection.fromLatLngToDivPixel(center),
        SW       = new google.maps.Point(
      		position.x - this.gridsize,
      		position.y + this.gridsize 
      	),
        NE       = new google.maps.Point(
      		position.x + this.gridsize,
      		position.y - this.gridsize
      	);
        
    this._setBounds(SW,NE);
    this.addMarker(origin);
  },
  
  contains: function() {
    return arguments[0] && this.getBounds().contains(arguments[0]);
  },
  
  hide: function() {
    if (this.clusterMarker != null) {
			this.clusterMarker.hide();
		}
  },
  
  show: function() {
    if (this.markers.size() == 1) {
			this.markers[0].setMap(this.map);
		
    }else if (this.markers.length > 1){
      this.markers.each(function(el) {
        el.setMap(null);
      });
			
			// Create marker
			if (this.clusterMarker == null) {
				this.clusterMarker = new GooglemapsClusterMarker({
          map           : this.map,
          markerCount   : this.getMarkerCount(),
          position      : this.getPosition(),
          styles        : this.styles,
          markerBounds  : this.getMarkerBounds()
        });
		  }
			
			// Show marker
			this.clusterMarker.show();
		}
    
  },
  
  // Markers
  addMarker: function() {
    this.markers.push(arguments[0]);
  },
  getMarkers: function() {
    return this.markers;
  },
  getMarkerCount: function() {
    return this.markers.size();
  },
  
  // Position
  getCenter: function() {
    return this.center;
  },
  _setCenter: function() {
    this.center = arguments[0];
  },
  getPosition: function() {
    return this.center;
  },
  _setPosition: function() {
    this.setCenter(arguments[0]);
  },
  
  // Bounds
  getMarkerBounds: function() {
    var bounds = new google.maps.LatLngBounds(
			this.getMarkers()[0].getPosition(),
			this.getMarkers()[0].getPosition()
		);
    this.getMarkers().each(function(el) {
			bounds.extend(el.getPosition());
		});
    var NE     = this.projection.fromLatLngToDivPixel(bounds.getNorthEast()),
        SW     = this.projection.fromLatLngToDivPixel(bounds.getSouthWest()),
        width  = (NE.x-SW.x).abs(),
        height = (NE.y-SW.y).abs();
		return [bounds,width,height];
  },
  
  getBounds: function() {
    return this.bounds;
  },
  _setBounds: function(SW,NE) {
    this.bounds = new google.maps.LatLngBounds(
  		this.projection.fromDivPixelToLatLng(SW),
  		this.projection.fromDivPixelToLatLng(NE)
  	);
  },
  
  nop: function() {}
  
});

function GooglemapsClusterMarker() {
	var param = arguments[0];
    
  this.map          = param.map;
  this.markerCount  = param.markerCount;
  this.position     = param.position;
	this.styles       = param.styles;
  
	this.markerBounds            = param.markerBounds[0];
  this.markerBoundsPixelWidth  = param.markerBounds[1];
  this.markerBoundsPixelHeight = param.markerBounds[2];
  
	this.style = null;
	this.div   = null;
	
  $H(this.styles).each(function(pair) {
    if (this.markerCount > pair.key) {
       this.style = pair.value;
    }else{
      return;
    }
  }.bind(this));
	this.setMap(this.map);
	this.draw();
};

GooglemapsClusterMarker.prototype = new google.maps.OverlayView();

GooglemapsClusterMarker.prototype.onAdd = function() {
  /*
  var pane = this.getPanes().overlayLayer;
      pane.appendChild(this.div);
  */
  
  var panes = this.getPanes();
      panes.overlayMouseTarget.appendChild(this.div);
  
  google.maps.event.addDomListener(this.div, 'click', function() {
  	this.map.fitBounds(this.markerBounds);
  }.bind(this));
};

GooglemapsClusterMarker.prototype.draw = function() {
	if (this.div == null) {
    this.div = new Element('div',{'class': 'googlemapsClusterMarker' }).update(this.markerCount).setStyle({
      position    : 'absolute',
      width       : this.style.width + 'px',
      height      : this.style.height + 'px',
      lineHeight  : this.style.height + 'px',
      background  : 'transparent url("' + this.style.image + '") 50% 50% no-repeat',
      color       : this.style.textColor,
      cursor      : 'pointer',
      textAlign   : 'center',
      fontWeight  : 'bold',
      zIndex      : this.style.zIndex || 0
    });
	}

	var position = this.getProjection().fromLatLngToDivPixel(this.position);
  this.div.setStyle({
    left: (position.x - parseInt(this.style.width / 2)) + 'px',
    top : (position.y - parseInt(this.style.height / 2)) + 'px'
  });
};

GooglemapsClusterMarker.prototype.hide = function() {
	this.div.hide();
};

GooglemapsClusterMarker.prototype.show = function() {
	this.div.show();
};
