if (typeof gHotSpotLocale != "object") {
	throw new Error("GHotSpot.js: You must load GHotSpotLocale.js!");
}

if (typeof GLocation != "function") {
	throw new Error("GHotSpot.js: You must load GLocation.js!");
}

function GHotSpot(element, mapElement, config) {
	this.element = element;
	this.mapElement = mapElement;
	this.config = $.extend({}, GHotSpot.defaultConfig, config || {});
	this.map = null;
	this.index = GHotSpot.instances.length;
	this.response = null; // stores the response object returned by The Google Maps API (GClientGeocoder.prototype.getLocations) - is set in the callback method
	this.results = []; // array containing the number (this.config.matches) of locations closest to the given address
	this.geocoder = new GClientGeocoder();
	if (this.config.countryCode) {
		this.geocoder.setBaseCountryCode(this.config.countryCode);
	}
	this.index = GHotSpot.instances.length;
	this.initialized = false;
}

GHotSpot.defaultConfig = {
	eventType: "submit", // which kind of event in the hotspot should update the map (could also be 'keyup')?
	noChars: 0, // if > 0 the maxlength attribute on the input field is set accordingly
	matches: 5, // number of matches to return to the user
	initialAddress: "", // must be specified (a non-empty string) and must be a location in the region/country on the map
	countryCode: "", // restricts the search to the specified country/region on the map - should be specified otherwise the user may end up anywhere
	paramName: "addr", // the query string parameter name in which a possible address is passed
	regExp: "", // a pattern (string) or a regular expression that the user input must match to query Google for an address/location
	initialZoomLevel: 0, // is used to set the zoom level of the map - should be 0 (the world) or a larger integer, see http://cfis.savagexi.com/articles/2006/05/03/google-maps-deconstructed
	markerEventType: "", // which kind of event on each marker should trigger the info window? - typical values are "click" and "mouseover" - will be executed in the scope of this GHotSpot instance and passed the GLocation instance
	markerEvents: null, // which kind of events should be associated with each marker? Example: {click: myFunction, mouseover: myOtherFunction} - will be executed in the scope of this GHotSpot instance and passed the GLocation instance
	infoWindowOptions: null, // object literal with options for the marker info window
	mapControls: null, // array of GMapControls
	gLocationIcon: null, // must be GIcon object (or null) or a function returning a GIcon object - if a function it will be passed the gLocation instance
	mapSearchIcon: null, // must be GIcon object (or null) or a function returning a GIcon object - if a function it will be passed the gLocation instance
	enableScrollWheelZoom: true // should the map be zoomed when scrolling the mouse?
}

GHotSpot.instances = [];

GHotSpot.newInstance = function(element, mapElement, config) {
	var instance = new GHotSpot(element, mapElement, config);
	GHotSpot.instances.push(instance);
	return instance;
}

GHotSpot.readAddressFromURL = function(paramName) {
	var addr = "";
	var query = location.search; // is the empty string if no ? in location.href, but includes the ? if present
	var index = query.indexOf(paramName);
	if (index > -1) {
		addr = query.substring(index + paramName.length + 1); // remove = too
		index = addr.indexOf('&');
		if (index > -1) {
			addr = addr.substring(0, index);
		}
	}
	return decodeURIComponent(addr);
}

GHotSpot.initGHotSpots = function() {
	for (var i = 0, l = GHotSpot.instances.length; i < l; i++) {
		GHotSpot.instances[i].init();
	}
}

GHotSpot.specialKeyCodes = [];

(function() {
	for (var i = 192; i < 256; i++) {
		GHotSpot.specialKeyCodes[i - 192] = i;
	}
})();

GHotSpot.getCaptionObject = function(caption, accesskeyClassName) {
	if (caption) {
		var obj = {};
		// find first occurrence of * and make sure it is not the last character and is not preceeded by another *
		var patternString = "\\*[a-z";
		for (var i = 0, l = GHotSpot.specialKeyCodes.length; i < l; i++) {
			patternString += String.fromCharCode(GHotSpot.specialKeyCodes[i]);
		}
		patternString += "]{1}";
		var found = new RegExp(patternString, "i").exec(caption);
		if (found && caption.indexOf('**') == -1) {
			obj['accesskey'] = found[0].substring(1);
			obj['accesskeyClassName'] = accesskeyClassName || "Accesskey";
			obj['plain'] = caption.replace('*', '');
			caption = caption.replace(found[0], '<span class="' + obj['accesskeyClassName'] + '">' + obj['accesskey'] + '<\/span>');
		}
		obj['caption'] = caption;
		return obj;
	}
	return null;
}

GHotSpot.formatMessage = function(msg, valuesArray, del) {
	var a = msg.split(del || gHotSpotLocale.msgDel);
	if (a.length == 1 || valuesArray == null || typeof valuesArray.length != "number" || a.length != valuesArray.length + 1) {
		return msg;
	}
	msg = "";
	for (var i = 0, l = valuesArray.length; i < l; i++) {
		msg += a[i] + valuesArray[i];
	}
	return msg + a[a.length - 1];
}

GHotSpot.prototype.toString = function() {
	return "[object GHotSpot] " + ((this.element.id != null) ? this.element.id : this.element);
}

GHotSpot.prototype.init = function() {
	if (!this.initialized) {
		if (typeof this.element == "string") {
			this.element = $(this.element)[0];
		}
		if (typeof this.mapElement == "string") {
			this.mapElement = $(this.mapElement)[0];
		}
		this.setup();
		var addr = this.readAddressFromURL();
		if (addr) {
			this.getLocation(addr);
			this.element.value = addr;
		}
		this.initialized = true;
	}
}

GHotSpot.prototype.readAddressFromURL = function() {
	return GHotSpot.readAddressFromURL(this.config.paramName);
}

GHotSpot.prototype.onSubmit = function(e) {
	if (e) {
		if (typeof e.preventDefault == "function") {
			e.preventDefault();
		} else {
			e.returnValue = false;
		}
	}
	if (this.validate()) {
		this.getLocation();
	}
}

GHotSpot.prototype.getValue = function() {
	var value = this.element.value;
	if (value && this.config.countryCode) {
		value += ", " + this.config.countryCode;
	}
	return value;
}

GHotSpot.prototype.validate = function() {
	var ok = true;
	if (this.config.regExp) {
		ok = (this.config.regExp.constructor == RegExp) ? this.config.regExp.test(this.element.value) : new RegExp(this.config.regExp).test(this.element.value);
	}
	if (!ok) {
		this.log(gHotSpotLocale.invalidAddress);
	}
	return ok;
}

GHotSpot.prototype.onKeyup = function(e) {
	if (this.validate()) {
		this.getLocation();
	}
}

GHotSpot.prototype.setup = function() {
	if (this.element && this.config.noChars > 0) {
		this.element.setAttribute("maxlength", "" + this.config.noChars);
	}
	if (this.mapElement) {
		this.map = new GMap2(this.mapElement);
		if (this.config.enableScrollWheelZoom) {
			this.map.enableScrollWheelZoom();
		}
		var controls = this.config.mapControls || [];
		for (var i = 0, l = controls.length; i < l; i++) {
  			this.map.addControl(controls[i]);
		}
		if (this.config.initialAddress) {
			if (this.config.initialAddress instanceof GLatLng) {
				this.initMap(this.config.initialAddress);
			} else {
				this.geocoder.getLocations(this.config.initialAddress, GEvent.callback(this, this.callbackInitMap));
			}
		}
	}
	var eType = this.config.eventType;
	if ("submit" == eType) {
		var form = this.element ? this.element.form : null;
		if (form) {	
			GEvent.addDomListener(form, "submit", GEvent.callback(this, this.onSubmit));
		}
	}
	else if (eType.indexOf("key") == 0) {
		$(this.element)[eType](GEvent.callback(this, this.onKeyup));
	}
}

GHotSpot.prototype.getLocation = function(value) {
	value = value || this.getValue();
	this.geocoder.getLocations(value, GEvent.callback(this, this.callback));
}

GHotSpot.prototype.callback = function(response) {
	this.response = response;
	if (response.Status.code != 200) {
		this.log(gHotSpotLocale.googleStatusObject["" + response.Status.code]);
		return;
	}
	var no = response.Placemark.length;
	if (no < 1) {
		this.log(gHotSpotLocale.noResults); // will probably never go here
	} else {
		if (this.updateSearchResults()) {
			this.calculate(0);
		}
	}
}

GHotSpot.prototype.calculate = function(placeIndex) {
	placeIndex = Math.max(parseInt(placeIndex, 10), 0);
	var oGLatLng = new GLatLng(this.response.Placemark[placeIndex].Point.coordinates[1], this.response.Placemark[placeIndex].Point.coordinates[0]);
	var results = [];
	for (var i = 0, l = GLocation.instances.length; i < l ; i++) {
		var gLocation = GLocation.instances[i];
		if (gLocation.gLatLng == null) {
			continue;
		}
		gLocation.distance = gLocation.gLatLng.distanceFrom(oGLatLng);
		results.push(gLocation);
	}
	results.sort(GLocation.compare);
	if (this.config.matches > 0) {
		this.results = results.slice(0, this.config.matches);
	}
	this.updateMap({center: oGLatLng});
	this.updateGLocationSearchResults(placeIndex);
}

GHotSpot.prototype.updateSearchResults = function() {
	function isValidCountryCode(code, placeMarks) {
		if (!code) {
			return true;
		}
		for (var i = 0, l = placeMarks.length; i < l; i++) {
			var mark = placeMarks[i];
			if (code.toLowerCase() == mark.AddressDetails.Country.CountryNameCode.toLowerCase()) {
				return true;
			}
		}
		return false;
	}
	var rv = isValidCountryCode(this.config.countryCode, this.response.Placemark);
	var htm = '';
	if (rv) {
		var no = this.response.Placemark.length;
		htm += '<dl><dt>' + GHotSpot.formatMessage(gHotSpotLocale.foundAddresses, [no, this.element.value]) + '<\/dt>';
		for (var i = 0; i < no; i++) {
			var place = this.response.Placemark[i];
			htm += '<dd>';
			htm += '<a href="" onclick="return GHotSpot.instances[' + this.index + '].onClick(event, ' + i + ');">' + place.address + '<\/a>';
			htm += '<\/dd>';
		}
		htm += '<\/dl>';
	} else {
		htm += gHotSpotLocale.wrongCountryCode;
	}
	this.log(htm);
	return rv;
}

GHotSpot.prototype.updateGLocationSearchResults = function(placeIndex) {
	var htm = '<dl><dt>' + GHotSpot.formatMessage(gHotSpotLocale.foundGLocations, [Math.min(this.config.matches, this.results.length), this.response.Placemark[placeIndex].address]) + '<\/dt>';
	for (var i = 0, l = this.results.length; i < l; i++) {
		var gLocation = this.results[i];
		htm += '<dd>';
		htm += '<a href="" onclick="return GHotSpot.instances[' + this.index + '].showGLocation(event, ' + i + ');" title="' + gLocation.getAddress() + '">' + gLocation.config.name + '<\/a>';
		htm += '<\/dd>';
	}
	htm += '<\/dl>';
	$("#divGLocationSearchResults").html(htm);
}

GHotSpot.prototype.onClick = function(e, placeIndex) {
	if (typeof e.preventDefault == "function") {
		e.preventDefault();
	} else {
		e.returnValue = false;
	}
	this.calculate(placeIndex);
	return false;
}

GHotSpot.prototype.showGLocation = function(e, index) {
	if (typeof e.preventDefault == "function") {
		e.preventDefault();
	} else {
		e.returnValue = false;
	}
	this.displayMarkerInfo(this.results[index]);
	return false;
}

// must return an object with property 'icon' referring to a GIcon instance
GHotSpot.prototype.resolveGIcon = function(gLocation, toResolve) {
	if (toResolve) {
		if (typeof toResolve == "function") {
			return toResolve(gLocation);
		}
		if (toResolve.icon && toResolve.icon.constructor == GIcon) {
			return {icon: icon};
		}
		return toResolve;
	}
	return null;
}

GHotSpot.prototype.updateMap = function(config) {
	config = $.extend({ center: null, markCenter: true, useBoundsCenter: true, zoomLevel: NaN }, config);
	this.map.clearOverlays();
	var bounds = new GLatLngBounds();
	var center = (config.center && config.center.constructor == GLatLng) ? config.center : null;
	if (center) {
		bounds.extend(center);
		var marker = new GMarker(center, {icon: this.config.mapSearchIcon});
		if (config.markCenter) {
			this.map.addOverlay(marker);
		}
	}
 	// this.results is an array of GLocation objects
	for (var i = 0, l = this.results.length; i < l; i++) {
		var gLocation = this.results[i];
		bounds.extend(gLocation.gLatLng);
		gLocation.marker = new GMarker(gLocation.gLatLng, this.resolveGIcon(gLocation, gLocation.config.mapIcon || this.config.gLocationIcon));
		this.map.addOverlay(gLocation.marker);
		if (this.config.markerEventType) {
			GEvent.bind(gLocation.marker, this.config.markerEventType, this, GEvent.callbackArgs(this, this.displayMarkerInfo, gLocation));
		}
		if (this.config.markerEvents) {
			var obj = this.config.markerEvents;
			for (var p in obj) {
				GEvent.bind(gLocation.marker, p, this, GEvent.callbackArgs(this, obj[p], gLocation));
			}
		}
	}
	center = config.useBoundsCenter ? bounds.getCenter() : center;
	if (center) {
		this.map.setCenter(center);
	}
	this.map.setZoom(config.zoomLevel || this.map.getBoundsZoomLevel(bounds));
}

GHotSpot.prototype.generateMarkerInfo = function(gLocation) {
	var htm = '<div class="vcard">';
	htm += '<p class="org">' + gLocation.config.name + '<\/p>';
	/*if (gLocation.config.firstname || gLocation.config.lastname) {
		htm += '<p class="fn n">';
		if (gLocation.config.firstname) {
			htm += '<span class="given-name">' + gLocation.config.firstname + '<\/span>';
		}
		if (gLocation.config.lastname) {
			if (gLocation.config.firstname) {
				htm += '&nbsp;';
			}
			htm += '<span class="family-name">' + gLocation.config.lastname + '<\/span>';
		}
		htm += '<\/p>';
	}*/
	htm += '<div class="adr">';
	if (gLocation.config.street) {
		htm += '<p class="street-address">' + gLocation.config.street + '<\/p>';
	}
	htm += '<p>';
	if (gLocation.config.zipcode) {
		htm += '<span class="postal-code">' + gLocation.config.zipcode + '<\/span>';
	}
	if (gLocation.config.city) {
		var sep = gLocation.config.zipcode ? ', ' : '';
		htm += '<span class="locality">' + sep + gLocation.config.city + '<\/span>';
	}
	htm += '<\/p>';
	if (gLocation.config.country) {
		htm += '<p class="country-name">' + gLocation.config.country + '<\/p>';
	}
	htm += '<\/div>';
	/*if (gLocation.config.phone) {
		htm += '<p class="tel phone">' + gLocation.config.phone + '</p>';
	}
	if (gLocation.config.email) {
		htm += '<p class="email"><a class="email" href="mailto:' + gLocation.config.email + '">' + gLocation.config.email + '<\/a><\/p>';
	}
	if (gLocation.config.url) {
		htm += '<p class="url"><a class="url fn" href="' + gLocation.config.url + '">' + gLocation.config.url + '<\/a><\/p>';
	}*/		
	htm += '<p class="AddToAddressBook"><a href="javascript:void(location.href=\'http://feeds.technorati.com/contacts/\'+encodeURIComponent(location.href));">' + gHotSpotLocale.addToAddressBook + '<\/a><\/p>';
	htm += '<\/div>';
	if (gLocation.warning) {
		htm += '<p class="Warning">' + gLocation.warning + '<\/p>';
	}
	return htm;
}

GHotSpot.prototype.displayMarkerInfo = function(gLocation) {
	gLocation.marker.openInfoWindowHtml(this.generateMarkerInfo(gLocation), gLocation.config.infoWindowOptions || this.config.infoWindowOptions || {});
}

GHotSpot.prototype.initMap = function(center, response) {
	this.map.setCenter(center, this.config.initialZoomLevel);
	if (typeof this.config.onInitMap == "function") {
		this.config.onInitMap.apply(this, [response]);
	}
}

GHotSpot.prototype.callbackInitMap = function(response) {
	if (response.Status.code != 200) {
		this.log(gHotSpotLocale.googleStatusObject["" + response.Status.code]);
		return;
	}
	var place = response.Placemark[0];
	this.initMap(new GLatLng(place.Point.coordinates[1], place.Point.coordinates[0]), response);
}

// override for customized logging
GHotSpot.prototype.log = function(msg) {
	$("#divSearchResults").html(msg);
	$("#divGLocationSearchResults").html('');
}

GHotSpot.prototype.filter = function(fn) {
	return $.grep(GLocation.instances, fn);
}

GHotSpot.prototype.plot = function(fn, config) {
	this.results = (typeof fn == "function") ? this.filter(fn) : GLocation.instances;
	this.updateMap(config);
}

$(document).ready(function() {
	if (GBrowserIsCompatible()) {
		GHotSpot.initGHotSpots();
	} else {
		alert('Your browser is not compatible with the Google Maps API!');
	}
});

$(window).bind("unload", GUnload);