// load version 2 of the maps api
google.load("maps", "2");

window.GOOGLE_LOADED = false;

google.setOnLoadCallback(function GoogleLoaded() {
	window.GOOGLE_LOADED = true;
});		

;(function() {

	google.helpers = {
		// api ref: http://code.google.com/apis/maps/documentation/
		// icon ref: http://groups.google.com/group/google-chart-api/web/chart-types-for-map-pins?pli=1
		scaledPinUrl: function google$helpers$scaledPinUrl(scale, rotation, fill, bold, text) {
			return String.format("http://chart.apis.google.com/chart?chst=d_map_spin&chld={0}|{1}|{2}|{3}|{4}|{5}", 
				(scale || 1),			// {0} - scale factor
				(rotation || 0),		// {1} - rotation (degrees)
				(fill || "FF0000"),		// {2} - fill color (hex)
				Math.round(20 * (scale || 1)),	// {3} - font size
				(bold ? "b" : "_"),		// {4} - font style
				(text || "X")			// {5} - text
			);
		},
		iconPinUrl: function google$helpers$iconPinUrl(icon, fill) {
			return String.format("http://chart.apis.google.com/chart?chst=d_map_pin_icon&chld={0}|{1}",
				icon,			// {0} - icon name
				(fill || "FFFF00")	// {1} - fill color (hex)
			);
		}
	}

	/////////////////////////////////////////////////////////////////////////////
	// geocode: creates a single geocoder object and provides a global method to 
	// retrieve lat/lon for an address, accepting success and failure callbacks.
	var geocoder; 
	function geocode(address, successOrCallback, failure){
		geocoder = geocoder || new google.maps.ClientGeocoder();

		geocoder.getLatLng(
			address,
			function GeocoderResponseRecieved(point) {
				if (point)
					successOrCallback(address, point);
				else if(failure && failure instanceof Function)
					failure(address);
				else
					successOrCallback(null);
			});
	}
	window.geocode = geocode;

	/////////////////////////////////////////////////////////////////////////////
	// MapControl: encapsulates visual representation of a map and any other 
	// associated UI components, such as a logging mechanism.
	var _mapCache = {};
	function MapControl(mapId, focussedMarkerOptions) {
		if (_mapCache[mapId])
			return _mapCache[mapId];

		this._mapId = mapId;
		this._map = new google.maps.Map2(document.getElementById(mapId));
		
		var customUI = this._map.getDefaultUI();
		customUI.controls.menumaptypecontrol = false;
		this._map.setUI(customUI);
		
		this._points = {};
		this._focussedMarkerOptions = focussedMarkerOptions || {};

		_mapCache[mapId] = this;

		window.MapRequestProxy.onCommand(mapId, function(commandName, options) {
			if (commandName == "focusMarker") {
				scrollIntoView("#" + mapId);
				var point = this._points[options];
				if (point) {
					point.focus();
					window.MapRequestProxy.sendCommand(this._mapId, "click", point._address);
				}
			}
			else if (commandName == "reset") {
				this.reset();
				if (this._currentPoint)
					this._currentPoint.unfocus();
				delete this._currentPoint;
			}
		});
	}
	MapControl.prototype = {
		logWarning: function MapControl$logWarning(message) {
			//console.warn(message);
		},
		logInfo: function MapControl$logInfo(message) {
			//console.info(message);
		},
		getMap: function MapControl$getMap() {
			return this._map;
		},
		getMaxLat: function MapControl$getMaxLat() {
			var max = null;
			for (var key in this._points) {
				var point = this._points[key].getValue();
				if (point) {
					var lat = point.lat();
					max = max ? (lat > max ? lat : max) : lat;
				}
			}
			return max;
		},
		getMinLat: function MapControl$getMinLat() {
			var min = null;
			for (var key in this._points) {
				var point = this._points[key].getValue();
				if (point) {
					var lat = point.lat();
					min = min ? (lat < min ? lat : min) : lat;
				}
			}
			return min;
		},
		getMaxLatFrom: function MapControl$getMaxLatFrom(point) {
			var maxDelta = null;
			var baseLat = point.lat();
			for (var key in this._points) {
				var point = this._points[key].getValue();
				if (point) {
					var lat = point.lat();
					var delta = Math.abs(baseLat - lat);
					maxDelta = Math.max(delta, maxDelta || 0);
				}
			}
			return maxDelta;
		},
		getMaxLng: function MapControl$getMaxLng() {
			var max = null;
			for (var key in this._points) {
				var point = this._points[key].getValue();
				if (point) {
					var lng = point.lng();
					max = max ? (lng > max ? lng : max) : lng;
				}
			}
			return max;
		},
		getMinLng: function MapControl$getMinLng() {
			var min = null;
			for (var key in this._points) {
				var point = this._points[key].getValue();
				if (point) {
					var lng = point.lng();
					min = min ? (lng < min ? lng : min) : lng;
				}
			}
			return min;
		},
		getMaxLngFrom: function MapControl$getMaxLngFrom(point) {
			var maxDelta = null;
			var baseLng = point.lng();
			for (var key in this._points) {
				var point = this._points[key].getValue();
				if (point) {
					var lng = point.lng();
					var delta = Math.abs(baseLng - lng);
					maxDelta = Math.max(delta, maxDelta || 0);
				}
			}
			return maxDelta;
		},
		getCenter: function MapControl$getCenter() {
			for (var key in this._points) {
				var point = this._points[key];
				if (point.getOption("center"))
					return point.getValue();
			}
			return null;
		},
		reset: function MapControl$reset() {
			var center = this.getCenter();
			var maxDelta = 0;
			if (center) {
				var latDelta = this.getMaxLatFrom(center);
				var lngDelta = this.getMaxLngFrom(center);
				maxDelta = Math.max(latDelta, lngDelta);
			}
			else {
				var maxLat = this.getMaxLat(), minLat = this.getMinLat();
				var latDelta = maxLat - minLat;
				
				var maxLng = this.getMaxLng(), minLng = this.getMinLng();
				var lngDelta = maxLng - minLng;
					
				maxDelta = Math.max(latDelta, lngDelta);
				
				center = new google.maps.LatLng(maxLat - (latDelta/2), maxLng - (lngDelta/2));
			}
					
			this._map.setCenter(center, 11 - Math.round(maxDelta * 3.25));
		},
		geocodeSuccess: function MapControl$geocodeSuccess(address, point) {
			this.logInfo("Found \"" + address + "\".");

			var mapPoint = this._points[address];
			mapPoint.setValue(point);
			
			if (!this._initialized) {
				this._map.setCenter(point, 12);
				this._initialized = true;
			}
			else {
				this.logInfo("new point encounted");
				this.reset();
			}
			
			this.logInfo("adding marker");
			
			var marker = mapPoint.getMarker();
			this._map.addOverlay(marker);
			
			this.logInfo("marker added");
		},
		geocodeFailure: function MapControl$geocodeFailure(address) {
			this.logWarning("Address \"" + address + "\" not found!");
		},
		add: function MapControl$add(address, sourceElements, options) {
			if (this._points[address])
				this._points[address].merge(sourceElements, options);
			else
				this._points[address] = new MapPoint(this, address, sourceElements, options);

			this.logInfo("Looking up \"" + address + "\".");

			var _this = this;
			geocode(address, 
				function GeocodeSuccessScope(address, point) { _this.geocodeSuccess(address, point); }, 
				function GeocodeFailedScope(address) { _this.geocodeFailure(address); });
			}
	}
	MapControl.create = function MapControl$create(mapId, focussedMarkerOptions) {
		return new MapControl(mapId, focussedMarkerOptions);
	}
	window.MapControl = MapControl;

	function MapPoint(mapControl, address, sourceElements, options) {
		if (!address || !(address.constructor == String))
			throw ("address is not valid");

		this._mapControl = mapControl;
		this._address = address;

		if (!sourceElements)
			this._sourceElements = [];
		else if (sourceElements instanceof Array)
			this._sourceElements = sourceElements;
		else if (sourceElements.constructor == String)
			this._sourceElements = [sourceElements];

		this._options = options || {};
			
		this._value = null;
	}
	MapPoint.prototype = {
		merge: function MapPoint$merge(sourceElements, options) {
			// TODO: check for duplicates
			while (sourceElements.length)
				// order doesn't matter
				this._sourceElements.push(sourceElements.pop());
			
			// copy new options to the existing point/marker
			for (var prop in options) {
				if (!this._options[prop])
					this._options[prop] = options[prop];
			}
		},
		getValue: function MapPoint$getValue() {
			return this._value;
		},
		setValue: function MapPoint$setValue(value) {
			this._value = value;
		},
		getOption: function MapPoint$getOption(name) {
			return this._options[name] || null;
		},
		updateSources: function MapPoint$updateSources(overrides) {
			overrides = overrides || {};

			for (var i = 0; i < this._sourceElements.length; i++) {
				var src = this._sourceElements[i];
				var j = $("#" + src);
				if (overrides["focus"] || this._options["focus"])
					j.addClass("focus");
				else
					j.removeClass("focus");
			}
		},
		getIconUrl: function MapPoint$getIconUrl(overrides) {
			overrides = overrides || {};

			if (overrides["icon"] || this._options["icon"]) {
				return google.helpers.iconPinUrl(overrides["icon"] || this._options["icon"], 
								overrides["fill"] || this._options["fill"]);
			}
			else {
				return google.helpers.scaledPinUrl(overrides["large"] || this._options["large"] ? 1.25 : 1,
								overrides["rotation"] || this._options["rotation"] || 0,
								overrides["fill"] || this._options["fill"],
								overrides["bold"] || this._options["bold"],
								overrides["text"] || this._options["text"]);
			}
		},
		unfocus: function() {
			var mapControl = this._mapControl;
			var map = mapControl.getMap();

			map.removeOverlay(this._marker);
			var icon = this._marker.getIcon();
			icon.image = this.getIconUrl();
			if (this._options["large"])
				icon.iconSize = new google.maps.Size(25, 42);
			else
				icon.iconSize = new google.maps.Size(20, 34);
			map.addOverlay(this._marker);
			this.updateSources();
		},
		focus: function() {
			var mapControl = this._mapControl;
			var map = mapControl.getMap();

			mapControl.logInfo("marker clicked");

			// revert the old focussed marker to default styling
			if (mapControl._currentPoint)
				mapControl._currentPoint.unfocus();

			// set the new focussed marker and point
			mapControl._currentPoint = this; 

			// overrided styling of the new focussed marker
			map.removeOverlay(this._marker);
			var icon = this._marker.getIcon();
			icon.image = mapControl._currentPoint.getIconUrl(mapControl._focussedMarkerOptions);
			if (mapControl._focussedMarkerOptions["large"] || mapControl._currentPoint._options["large"])
				icon.iconSize = new google.maps.Size(25, 42);
			else
				icon.iconSize = new google.maps.Size(20, 34);
			map.addOverlay(this._marker);

			this.updateSources(mapControl._focussedMarkerOptions);

			mapControl._map.setCenter(this._value);
		},
		getMarker: function() {
			if (!this._marker) {
				var icon = new google.maps.Icon(G_DEFAULT_ICON);
				icon.image = this.getIconUrl();
				if (this._options["large"])
					icon.iconSize = new google.maps.Size(25, 42);
				
				var marker = this._marker = new google.maps.Marker(this._value, { icon: icon });

				this.updateSources();

				var _this = this;
				google.maps.Event.addListener(marker, "click", function() {
					_this.focus();
					window.MapRequestProxy.sendCommand(_this._mapControl._mapId, "click", _this._address);
				});
			}
			
			return this._marker;
		}
	}

	/////////////////////////////////////////////////////////////////////////////
	// MapRequestProxy: allows script to create map requests for a specific 
	// map regardless of whether or not the element exists or has been 
	// initialized as a map control.
	var _requests = {};
	function MapRequestProxy() {}
	MapRequestProxy.prototype = {
		register: function MapRequestProxy$register(mapId, groupName, address, targetId, options) {
			if (!_requests[mapId])
				_requests[mapId] = {};

			if (_requests[mapId][groupName]) {
				_requests[mapId][groupName].Targets.push(targetId);
					
				// copy new options to the existing point/marker
				for (var prop in options) {
					if (!_requests[mapId][groupName].Options[prop])
						_requests[mapId][groupName].Options[prop] = options[prop];
				}
			}
			else
				_requests[mapId][groupName] = { Address: address, Targets: [targetId], Options: options };
		},
		getRequestsForMap: function MapRequestProxy$getRequestsForMap(id) {
			return _requests[id] || null;
		},
		onCommand: function MapRequesProxy$onCommand(mapId, handler) {
			this._handlers = this._handlers || {};
			this._handlers[mapId] = this._handlers[mapId] || [];
			this._handlers[mapId].push(handler);
		},
		sendCommand: function MapRequestProxy$sendCommand(mapId, commandName, options) {
			if (this._handlers && this._handlers[mapId]) {
				for (var i = 0; i < this._handlers[mapId].length; i++) {
					var handler = this._handlers[mapId][i];
					var map = new MapControl(mapId);
					handler.call(map, commandName, options);
				}
			}
		}
	}
	window.MapRequestProxy = new MapRequestProxy();

}) ();

