var GoogleMap = Class.create();

GoogleMap.prototype = {

	// initialisation: create map, add map controls, set base icons, and set initial parameters for displaying markers
	initialize: function(target, point_array) {
		var targ = this, pos_array = [];

		targ.map = new GMap2(target);
		targ.point_array = point_array;
		targ.cluster_array = [];

		if (!isArray(targ.point_array)) { return false; }

		targ.point_array.each(function(point) {
			pos_array.push({'lat':point.lat, 'lng':point.lng});
		});
		var bounds = targ.get_map_bounds(pos_array);

		targ.map.addControl(new ZoomControl());
		targ.map.addControl(new TypeControl());
		targ.map.setCenter(bounds.center,  targ.map.getBoundsZoomLevel(bounds.outer), G_NORMAL_MAP);

		GEvent.addListener(targ.map, 'zoomend', function() { targ.reset_markers(); });
		GEvent.addListener(targ.map, 'moveend', function() { targ.load_markers(); });


		targ.baseIcon = new GIcon();
		targ.baseIcon.shadowSize = new GSize(25,25);
		targ.baseIcon.iconSize = new GSize(25,25);
		targ.baseIcon.iconAnchor = new GPoint(12,12);
		targ.baseIcon.infoWindowAnchor = new GPoint(12,12);

		targ.load_markers();
	},

	// returns the boundaries of the map using all points that are to be displayed on the map
	get_map_bounds: function (points) {
		var max_lat = 0, max_lng = 0, min_lat = 0, min_lng = 0;
		for (var i = 0; i < points.length; i++) {
			if (points[i].lat < min_lat || min_lat == 0) { min_lat = points[i].lat; }
			if (points[i].lat > max_lat || max_lat == 0) { max_lat = points[i].lat; }
			if (points[i].lng < min_lng || min_lng == 0) { min_lng = points[i].lng; }
			if (points[i].lng > max_lng || max_lng == 0) { max_lng = points[i].lng; }
		}
		var avg_lat = (max_lat + min_lat) / 2;
		var avg_lng = (max_lng + min_lng) / 2;
		return {'center': new GLatLng(avg_lat, avg_lng), 'outer': new GLatLngBounds(new GLatLng(min_lat, min_lng), new GLatLng(max_lat, max_lng))};
	},

	// sets new zoom level for map, and centers the map on a given point
	zoom_on_point: function(point, zoom) {
		this.map.setCenter(point, zoom);
	},

	// called on zoom. reset point positions and set to defaults, remove clusters (otherwise, the clusters will build up)
	reset_markers: function() {
		var targ = this;

		targ.cluster_array.each(function(cluster){
			if (cluster.marker) {
			    targ.remove_marker(cluster.marker);
			}
		});
		targ.cluster_array = [];

		targ.point_array.each(function(point){
			if (point.lat_orig && point.lng_orig) {
			    point.lat = point.lat_orig;
				point.lng = point.lng_orig;
				point.lat_orig = null;
				point.lng_orig = null;
			}
			point.inCluster = false;
		});
	},

	// removes a marker from the map, and removes all event listeners associated with the marker
	remove_marker: function(marker) {
		var targ = this;
		GEvent.clearInstanceListeners(marker);
		targ.map.removeOverlay(marker);
	},

	load_markers: function() {
		var targ = this;

		var map_bounds = targ.map.getBounds();

		/*
		var debug_div = document.createElement('div');
		debug_div.id = 'gmap-debug';
		debug_div.style.position = 'absolute';
		debug_div.style.background = 'yellow';
		debug_div.innerHTML = $H(targ.map.getBounds()) + 'z: '+targ.map.getZoom();
		if ($('gmap-debug')) {
			document.body.replaceChild(debug_div, $('gmap-debug'));
		} else {
			document.body.appendChild(debug_div);
		}
		*/

		// remove all non-visible points
		targ.point_array.each(function(point){
			if (map_bounds.contains(new GLatLng(point.lat, point.lng))) {
			    point.visible = true;
			} else {
				point.visible = false;
				if (point.marker) {
				    targ.remove_marker(point.marker);
					point.marker = null;
				}
			}
		});

		if (targ.map.getZoom() > 15) {
		    targ.spiral_points();
		} else {
			targ.cluster_points();
		}

		// create marker for all visible points (that are not in clusters and that dont already have markers showing)
		targ.point_array.each(function(point){
			if(point && !point.inCluster && point.visible && !point.marker) {
				var icon_image = 'http://www.ourproperty.co.uk/images/pros-signs/mini/';
				icon_image += point.icon.replace(new RegExp('^icon-', 'i'), 'icon-mini-').replace(new RegExp('.gif$', 'i'), '.png');
				point.marker = targ.create_marker(icon_image, point);
			}
		});

	},

	// create a marker for a point and returns a reference to the marker (this function is task-specific, so for for other applications, this needs changing to fit needs)
	create_marker: function(image, details) {
		var targ = this;
		var point = new GPoint(details.lng, details.lat);

		var icon = new GIcon(targ.baseIcon);
		if (image) {
		    icon.image = image;
			icon.shadow = image;
		}
		var marker = new GMarker(point, icon);
		var bus_types = details.business.join(', ');
		var html = '<div class="google-marker-popup"><img src="http://www.ourproperty.co.uk/images/pros-signs/'+details.icon+'" style="width:49px; height:52px; float:left; margin-right:10px;"/><strong>'+details.name+'</strong><br />'+(details.add_num ? details.add_num+', ' : '')+details.addr1+'<br />'+details.city+'<br />'+details.postcode+'<div style="margin-top: 10px; font-size: 11px; background: #F8F1D6; padding: 5px;"><strong>Business Areas:</strong><br />'+bus_types+'</div></div>';
	
		GEvent.addListener(marker, 'click', function() { marker.openInfoWindowHtml(html); });
		targ.map.addOverlay(marker);

		return marker;
	},

	// for high zoom levels - arrange points with identical position into 'shells' around a center point
	spiral_points: function() {
		var targ = this;
		var px_per_degree = Math.pow(2,targ.map.getZoom()); // google uses this function (or similar) to determine how many pizels are in each degree
		var shell_points = [1,8,14,20,30]; // number of points on each level
		var shell_dist = 28; // distance between each shell level

		var diff_lat = shell_dist / px_per_degree;
		var diff_lng = shell_dist / (px_per_degree * 0.77162458338772); // adjustment to longtitude

		var FixPoint = Class.create();

		FixPoint.prototype = {
			initialize: function(loc, lat, lng) {
				this.loc = loc;
				this.lat = lat;
				this.lng = lng;
				this.ring = 0;
				this.pos = 1;
			}
		}

		var fix_array = [];

		targ.point_array.each(function(point){
			if (point && point.visible) {
				// detect whether the postcode already exists in an object in the conflict array, if so, then return the first and (and only) object
				var conflict = fix_array.detect(function(fix){
					return (fix.lat == point.lat && fix.lng == point.lng);
				});
				if (conflict) {
					if (conflict.pos < shell_points[conflict.ring]) { // still room in the current ring, so add to the ring
						conflict.pos++;
					} else { // no more room in current ring, so increase ring number and set as first position
						conflict.ring++;
						conflict.pos = 1;
					}
					// now adjust the position of the point
					point.lat_orig = point.lat;
					point.lng_orig = point.lng;
					point.lat = conflict.lat + ((diff_lat*conflict.ring) * Math.sin(conflict.pos * (2 * Math.PI / (shell_points[conflict.ring]))));
					point.lng = conflict.lng + ((diff_lng*conflict.ring) * Math.cos(conflict.pos * (2 * Math.PI / (shell_points[conflict.ring]))));
				} else {
					// if no conflict (ie a new location), then add the point to the conflict array and leave position as is (in the middle)
					fix_array.push(new FixPoint(point.postcode, point.lat, point.lng));
				}
			}
		});
	},

	// for low zoom levels - when points are in highly dense locations, arrange points into clusters that zoom map when clicked (thus creating one point, instead of 30 etc)
	cluster_points: function() {

		var targ = this;
		var grid_size = 5; // ie 5 rows
		var map_bounds = targ.map.getBounds();
		var min_markers_per_cluster = 5;
		var bound_ne = map_bounds.getNorthEast();
		var bound_sw = map_bounds.getSouthWest();
		var lat_range = bound_ne.lat() - bound_sw.lat();

		var grid_lat = lat_range / grid_size;
		var grid_lng = grid_lat / Math.cos((bound_ne.lat() + bound_sw.lat()) / 2 * Math.PI / 180);

		var Cluster = Class.create();
		Cluster.prototype = {
			// important: if this object declaration is moved outside of this function, then the grid_lat and grid_lng must be included in the object initialize call
			initialize: function(lat, lng) {
				this.bounds = new GLatLngBounds(new GLatLng(lat, lng), new GLatLng(lat + grid_lat, lng + grid_lng));
				this.markers = [];
				this.marker_conflict = false;
				this.lat_combined = 0, lat = 0;
				this.lng_combined = 0, lng = 0;
			}
		}

		// create a new cluster area object for every grid in the map
		for (var lat = bound_sw.lat(); lat <= bound_ne.lat(); lat += grid_lat) {
		    for (var lng = bound_sw.lng(); lng <= bound_ne.lng(); lng += grid_lng) {
		        targ.cluster_array.push(new Cluster(lat, lng));
		    }
		}

		// put all visible point into a cluster
		targ.point_array.each(function(point){
			if (point && !point.inCluster && point.visible) {
			    targ.cluster_array.each(function(cluster){
					if (cluster && cluster.bounds.contains(new GLatLng(point.lat, point.lng))) {
					    var conflict = cluster.markers.detect(function(fix){
							return fix.postcode == point.postcode;
						});
						if (conflict) {
						    cluster.marker_conflict = true;
						}
						cluster.markers.push(point);
						cluster.lat_combined += point.lat;
						cluster.lng_combined += point.lng;
						point.inCluster = true;
						if(point.marker) {
							targ.remove_marker(point.marker);
							point.marker = null;
						}
					}
				});
			}
		});

		// remove clusters that have less than the min number of points (and have not position conflict) and also reset the inCluster flag of all points in the cluster
		targ.cluster_array.each(function(cluster, pos){
			if (cluster && cluster.markers.length < min_markers_per_cluster && cluster.marker_conflict == false) {
				cluster.markers.each(function(point) {
					point.inCluster = false;
				});
			    targ.cluster_array[pos] = null;
			}
		});
		targ.cluster_array = targ.cluster_array.compact();

		targ.cluster_array.each(function(cluster){
			cluster.lat = cluster.lat_combined / cluster.markers.length;
			cluster.lng = cluster.lng_combined / cluster.markers.length;
			if (cluster && map_bounds.contains(new GLatLng(cluster.lat, cluster.lng))) {
				cluster.visible = true;
			} else {
				cluster.visible = false;
			}
		});

		// create a marker for each remaining cluster
		targ.cluster_array.each(function(cluster){
			if (cluster.marker) {
				targ.remove_marker(cluster.marker);
			} else {
				var icon = new GIcon(targ.baseIcon);
				icon.image = 'http://www.ourproperty.co.uk/images/pros-signs/mini/icon-zoom.png';
				icon.shadow = 'http://www.ourproperty.co.uk/images/pros-signs/mini/icon-zoom.png';
				cluster.marker = new GMarker(new GLatLng(cluster.lat, cluster.lng), icon);
			}

			if (cluster.visible) {
				GEvent.addListener(cluster.marker, 'click', function() { targ.zoom_on_point(new GLatLng(cluster.lat, cluster.lng), targ.map.getZoom()+2); });
				targ.map.addOverlay(cluster.marker);
			} else {
				targ.remove_marker(cluster.marker);
			}

		});

	}

}