/**
 * General geolocation functionality
 * The "geolocation" namespace
 */
var geolocation = {};


/**
 * Internationalization strings
 */
geolocation.ITEMS_HERE = 'items (click for details)';


/**
 * Markers and clustering
 */
geolocation.Cluster = function (o) {
	o = o || {};
	var self = this;

	this.o = $.extend({
		debug: false,			// set to "true" to enable performance profiling and verbose debug logging
		OFFSET: 268435456,		// the "center" of the flat world map, in pixels
		RADIUS: 85445659.4471,	// OFFSET / Math.PI
		clusterDistance: 25,	// pixels; the min. distance between two points before they get clustered
		map: null,				// a google.maps.Map() instance
		points: [],				// an array of objects, containing at least a .latitude and .longitude properties
		currentZoom: o.map && o.map.getZoom? o.map.getZoom() : 0,
		markerClickHandler: function () {}
	}, o);

	// the (clustered or not) markers will reside here
	this.markers = {};

	// hook to the zoom_changed event
	if (this.o.map) {
		google.maps.event.addListener(this.o.map, 'zoom_changed', function() { self.monitorZoom.apply(self, arguments); });
	}

	return this;
};

geolocation.Cluster.prototype.lngToX = function (lng) {
    return Math.round(this.o.OFFSET + this.o.RADIUS * lng * Math.PI / 180.0);        
};

geolocation.Cluster.prototype.latToY = function (lat) {
    return Math.round(this.o.OFFSET - this.o.RADIUS * 
                Math.log((1 + Math.sin(lat * Math.PI / 180.0)) / 
                (1 - Math.sin(lat * Math.PI / 180.0))) / 2.0);
};

geolocation.Cluster.prototype.pixelDistance = function (x1, y1, x2, y2, zoom) {
    return Math.sqrt(Math.pow((x1 - x2), 2) + Math.pow((y1 - y2), 2)) >> (21 - zoom);
};

geolocation.Cluster.prototype.add = function (o) {
	if (!this.o.map) return null;
	o = o || {};
	var x = this.lngToX(o.lng),
		y = this.latToY(o.lat),
		zoom = o.zoom !== undefined? o.zoom : this.o.map.getZoom(),
		cluster = null;
	if (this.markers[zoom] === undefined) {
		this.markers[zoom] = [];
	}
	for (var i = 0; i < this.markers[zoom].length; i++) {
		if (this.pixelDistance(x, y, this.markers[zoom][i].x, this.markers[zoom][i].y, zoom) <= this.o.clusterDistance) {
			cluster = i;
			break;
		}
	}
	if (cluster !== null) {
		var m = this.markers[zoom][cluster];
		m.points.push(o);
		if (m.type != 'cluster') {
			// change the type of this item to "cluster"
			m.type = 'cluster';
			// remove the old marker
			(m.pin && m.pin.setMap(null));
			(m.listener && google.maps.event.removeListener(m.listener));
			// create a new marker, representing the cluster
			m.pin = new google.maps.Marker({
				position: new google.maps.LatLng(m.lat, m.lng),
				map: this.o.map,
				icon: new google.maps.MarkerImage(
					'/images/marker_green.png',
					new google.maps.Size(20, 34),
					new google.maps.Point(0, 0),
					new google.maps.Point(10, 34)
				),
				shadow: new google.maps.MarkerImage(
					'/images/shadow50.png',
					new google.maps.Size(37, 34),
					new google.maps.Point(0, 0),
					new google.maps.Point(10, 34)
				)
			});
			m.pin.self = m;
			m.pin.obj = o.obj;
			// hook to the marker's click event
			m.listener = google.maps.event.addListener(m.pin, 'click', this.o.markerClickHandler);
		}
		m.pin.setTitle(m.points.length + ' ' + geolocation.ITEMS_HERE);
		this.markers[zoom][cluster] = m;
		return m;
	} else {
		var m = {
			type: 'marker',
			pin: new google.maps.Marker({
				position: new google.maps.LatLng(o.lat, o.lng),
				map: this.o.map,
				title: o.obj? o.obj.title : ''
			}),
			x: x,
			y: y,
			lat: o.lat,
			lng: o.lng,
			points: [o]
		};
		m.pin.self = m;
		m.pin.obj = o.obj;
		m.listener = google.maps.event.addListener(m.pin, 'click', this.o.markerClickHandler);
		this.markers[zoom].push(m);
		return m;
	}
	return null;
};

geolocation.Cluster.prototype.build = function(o) {
	if (!this.o.map) return null;
	if (this.o.debug) {
		var start = new Date();
	}
	o = o || {};
	var returning = null,
		zoom = o.zoom !== undefined? o.zoom : this.o.map.getZoom();
	if (o.bounds !== undefined && o.bounds.extend) {
		for (var i = 0; i < this.o.points.length; i++) {
			var m = this.add({
				lat: this.o.points[i].latitude,
				lng: this.o.points[i].longitude,
				obj: this.o.points[i],
				zoom: zoom
			});
			if (m && m.pin) {
				o.bounds.extend(m.pin.getPosition());
			}
		}
		returning = o.bounds;
	} else {
		for (var i = 0; i < this.o.points.length; i++) {
			this.add({
				lat: this.o.points[i].latitude,
				lng: this.o.points[i].longitude,
				obj: this.o.points[i],
				zoom: zoom
			});
		}
	}
	if (this.o.debug) {
		var end = new Date();
		this.log('build(zoom=' + zoom + '): ' + (end.getTime() - start.getTime()) / 1000.0 + 's for ' + this.o.points.length + ' points, ' + this.markers[zoom].length + ' markers.');
	}
	return returning;
};

geolocation.Cluster.prototype.monitorZoom = function (e) {
	if (!this.o.map) return;
	var zoom = Math.max(this.o.map.getZoom(), 0);
	if (this.o.currentZoom == zoom) {
		return;
	}
	if (this.markers[this.o.currentZoom] !== undefined) {
		// hide the old markers, if any
		for (var i = 0; i < this.markers[this.o.currentZoom].length; i++) {
			this.markers[this.o.currentZoom][i].pin.setMap(null);
		}
	}
	if (this.markers[zoom] === undefined && this.o.points) {
		// no markers/clusters are cached for this zoom level, so rebuild them
		// note: they will be automatically shown on the map
		this.build({zoom: zoom});
	} else {
		// show the cached markers for the new zoom level
		if (this.o.debug) {
			this.log('build(zoom=' + zoom + '): CACHED');
		}
		for (var i = 0; i < this.markers[zoom].length; i++) {
			this.markers[zoom][i].pin.setMap(this.o.map);
		}
	}
	this.o.currentZoom = zoom;
};

geolocation.Cluster.prototype.log = function (msg) {
	if (window.console && window.console.log) {
		try {
			console.log.apply(this, arguments);
		} catch (ex) {
			console.log(msg);
		}
	}
};



/**
 * Perform a (reverse or not) geocoding request and do sth. with the results
 */
geolocation.geocode = function (o) {
	// options initialization
	o = o || {};
	o = $.extend({
		map: null,
		marker: null,
		address: '',
		callback: null,
		geocoder: null,
		searchField: null,
		loaderTarget: o.searchField,
		searchFieldIdleText: 'Search for an address'
	}, o);
	if (!o.geocoder) {
		o.geocoder = new google.maps.Geocoder();
	}
	// place the search query, if the input is reasonable
	if (o.geocoder && o.address != '' && o.address != o.searchFieldIdleText) {
		if (o.loaderTarget) {
			showLoader(o.loaderTarget);
		}
		o.geocoder.geocode({ address: o.address }, function (results, status) {
			(o.callback && o.callback.apply && o.callback.apply(this, arguments));
			if (o.loaderTarget) {
				hideLoader(o.loaderTarget);
			}
			if (status == google.maps.GeocoderStatus.OK && results.length > 0) {
				if (status != google.maps.GeocoderStatus.ZERO_RESULTS) {
					if (o.map) {
						// ensure good visibility of the resulting location
						var recommendedViewport = results[0].geometry.viewport,
							currentViewport = o.map.getBounds();
						if (recommendedViewport && currentViewport) {
							if (
								!recommendedViewport.contains(currentViewport.getSouthWest()) ||
								!recommendedViewport.contains(currentViewport.getNorthEast()) ||
								!currentViewport.contains(results[0].geometry.location)
								) {
								o.map.fitBounds(recommendedViewport);
								o.map.setCenter(results[0].geometry.location);
							}
						}
					}
					if (o.marker) {
						// move the marker to the resulting location
						o.marker.setPosition(results[0].geometry.location);
					}
				}
			} else {
				// TODO: translate
				var msg = '';
				switch (status) {
					case google.maps.GeocoderStatus.ZERO_RESULTS:
						msg = 'There were no results to your search query. Please revise it and try again.';
					break;

					case google.maps.GeocoderStatus.INVALID_REQUEST:
						msg = 'Your search request is invalid. Please reformat it and try again.';
					break;
					
					default:
						msg = 'Your search request could not be serviced due to technical difficulties. Please try again later.';
					break;
				}
				if (msg) {
					alert(msg);
				}
			}
		});
	}
};


/**
 * A wrapper helper class to assist in the map's creation and manipulation
 */
geolocation.Map = function (o) {
	var self = this;
	self._o = o || {};

	self._canvas = o.canvas;
	delete o.canvas;

	// option defaults
	self._o = $.extend({
		mapTypeId: google.maps.MapTypeId.ROADMAP,
		mapTypeControl: true,
		mapTypeControlOptions: {style: google.maps.MapTypeControlStyle.DROPDOWN_MENU},
		navigationControl: true,
		navigationControlOptions: {style: google.maps.NavigationControlStyle.SMALL},
		zoom: 2,
		center: new google.maps.LatLng(0, 0)
	}, self._o);

	// create a helper, to give us access to the MapCanvasProjection instance
	function ProjectionHelperOverlay(map) {
		this.setMap(map);
	}
	ProjectionHelperOverlay.prototype = new google.maps.OverlayView();
	ProjectionHelperOverlay.prototype.draw = function () {
		if (!this.ready) {
			this.ready = true;
			google.maps.event.trigger(this, 'ready');
		}
	};

	// the map
	self._map = new google.maps.Map(self._canvas, self._o);

	// a marker
	self._marker = new google.maps.Marker({
		title: 'Drag to move',
		map: self._map
	});
	self._marker.setDraggable(true);
	google.maps.event.addListener(self._marker, 'position_changed', function (e) {
		if (self._marker.getPosition()) {
			self._marker.setVisible(true);
		} else {
			self._marker.setVisible(false);
		}
	});

	// overlay helper
	self._helper = new ProjectionHelperOverlay(self._map);
	self._overlay = null;

	// pins code
	var markerTimer = null;
	self._markerTimeout = 200; // ms

	setTimeout(function () {
		self._overlay = self._helper.getProjection();
		// fire the center_changed event
		self._map.setCenter(self._o.center);
	}, 1);

	google.maps.event.addListener(self._map, 'dblclick', function (e) {
		try { clearTimeout(markerTimer); } catch (e) {}
	});

	google.maps.event.addListener(self._map, 'click', function (e) {
		try { clearTimeout(markerTimer); } catch (err) {}
		markerTimer = setTimeout(function () {
			if (!self._overlay) {
				self._overlay = self._helper.getProjection();
			}
			if (self._overlay) {
				var scrollTop = window.pageYOffset || document.body.scrollTop,
					scrollLeft = window.pageXOffset || document.body.scrollLeft;
				var point = e.latLng;
				if (point) {
					self._marker.setPosition(point);
				}
			}
		}, self._markerTimeout);
	});

	return self;
};


geolocation.Map.prototype.resized = function () {
	if (this._map) {
		google.maps.event.trigger(this._map, 'resize');
	}
};


