var kEmptyHTMLMessage = '<Enter a description>';

function addMargin(text) {
	// Ok, I'd better explain this to myself:
	// The "g" means search globally, ie, find all of the matches in the input string
	// The parens mean take the thing there and put them into the $x reference
	// The question mark is the one that I really want to remember; placing it directly
	//	after a "quantifier" (in this case the asterisk, which quantifies the number of "."
	//	to match) tells RegExp to be "ungreedy", that is, to take the first acceptable match
	//	rather than finding the last "]" in the string.
	var re = new RegExp("\\[(.*?)\\]\\{(.*?)\\}", "g");
	var anchor = '<a href="$2" target="blank">$1</a>';
	
	var urlized = text.replace(re, anchor);

	return '<p>' + urlized + '</p>';
}

// Note default parameters
function cMarker(inMap, latLng, infoWindowOpenCallback, infoWindowCloseCallback, icon, markerHTML, announce) {
	this.cMarker = function(inMap, latLng, infoWindowOpenCallback, infoWindowCloseCallback,
							icon, markerHTML, announce) {
		this.mMap = inMap;
		this.mLatLng = latLng;
		this.mInfoWindowOpenCallback = infoWindowOpenCallback;
		this.mInfoWindowCloseCallback = infoWindowCloseCallback;
		this.mMarkerHTML = null;
		
		this.mIcon = null;
		this.mGoogleIcon = null;
		this.mMarker = null;
		this.mOpenListener = null;
		this.mCloseListener = null;
		
		var useHTML = ((markerHTML.length > 0) ? markerHTML : kEmptyHTMLMessage);

		this.makeMarker(icon, useHTML, announce);
	}
	
	this.destruct = function() {
		if(this.mOpenListener != null)
			GEvent.removeListener(this.mOpenListener);
			
		if(this.mCloseListener != null)
			GEvent.removeListener(this.mCloseListener);
			
		this.mMap.removeOverlay(this.mMarker);
	}
	
	this.hasHTML = function() {
		return this.mMarkerHTML != kEmptyHTMLMessage;
	}

	this.showPopup = function(overlay, html) {
		var popup = $(".popup");
		var contents = $(".popup_contents");
		var root = popup.parent()[0];
		var elm = $("#map")[0];
		var p = map.fromLatLngToContainerPixel(overlay.getLatLng());
		var i;
	
		contents.css({"width": ""});
		contents.empty().append(html);
	
		p.x -= Math.floor(popup[0].offsetWidth / 2);
		p.y -= popup[0].offsetHeight - 40;
		p.x += elm.offsetLeft;
		p.y += elm.offsetHeight;
	
		popup.css({"visibility": "", "left": String(p.x) + "px", "top": String(p.y) + "px"});
	}

	this.makeMarker = function(icon, markerHTML, announce) {
		var that = this;

		this.mIcon = icon;
		this.mGoogleIcon = new GIcon();
		this.mGoogleIcon.image = 'http://' + serverName + '/vamos/images/mapicons/' + icon.mFilename;
		this.mGoogleIcon.iconSize = new GSize(32, 32);
		this.mGoogleIcon.iconAnchor = (icon.mAnchorAtCenter ? new GPoint(15, 15) : new GPoint(15, 31));
		this.mGoogleIcon.infoWindowAnchor = new GPoint(15, 0);

		var markerOptions = {icon : this.mGoogleIcon};

		this.mMarker = new GMarker(this.mLatLng, markerOptions);
		this.mMarkerHTML = markerHTML;
		var encodedHTML = htmlentities(this.mMarkerHTML);

		if(this.mInfoWindowOpenCallback != null)
			this.mOpenListener = GEvent.addListener(this.mMarker, "infowindowopen", this.mInfoWindowOpenCallback);
		else
			this.mOpenListener = GEvent.addListener(this.mMarker, "click", function(point) {
				that.showPopup(this, addMargin(encodedHTML));
			});

		if(this.mInfoWindowCloseCallback != null)
			this.mCloseListener = GEvent.addListener(this.mMarker, "infowindowclose", this.mInfoWindowCloseCallback);

		this.mMap.addOverlay(this.mMarker);

		//this.mMarker.bindInfoWindowHtml(addMargin(encodedHTML));
		
		//if(announce)
			//this.mMarker.openInfoWindowHtml(addMargin(encodedHTML));
	}
	
	this.setIcon = function(icon) {
		if(this.mMarker != null)
			this.destruct();
			
		this.makeMarker(icon, this.mMarkerHTML);
	}
	
	// Note default parameter
	this.setMarkerDescription = function(description) {
		var show = defaultParameter(arguments, 2, true);

		this.mMarkerHTML = (description.length ? description : kEmptyHTMLMessage);
		this.mMarker.bindInfoWindowHtml(addMargin(htmlentities(this.mMarkerHTML)));
		this.show(show);
	}
	
	// Note default parameter
	this.show = function() {
		var show = defaultParameter(arguments, 1, true);
		
		if(show)
			this.mMarker.openInfoWindowHtml(addMargin(htmlentities(this.mMarkerHTML)));
		else
			this.mMarker.closeInfoWindow();
	}
	
	this.urlencode = function(myIndex) {
		var ret =
			'&m' + myIndex + 'Lat=' + this.mLatLng.lat() +
			'&m' + myIndex + 'Lng=' + this.mLatLng.lng() +
			'&m' + myIndex + 'HTML=' + (this.hasHTML() ? encodeURIComponent(this.mMarkerHTML) : '') +
			'&m' + myIndex + 'Icon=' + this.mIcon.mDBID;
			
		return ret;
	}

	this.cMarker(inMap, latLng, infoWindowOpenCallback, infoWindowCloseCallback, icon, markerHTML, announce);
}

function cRoutePoint(inMap) {
	this.mMap = inMap;
	this.mLatLng = defaultParameter(arguments, 2, null);
	this.mMarkerHTML = defaultParameter(arguments, 3, '');
	this.mIsVisible = defaultParameter(arguments, 4, true);
	this.mIsWaypoint = defaultParameter(arguments, 5, true);
	this.mIsRouteLinePoint = defaultParameter(arguments, 6, true);
	this.mIsFromClick = defaultParameter(arguments, 7, true);
	this.mIcon = null;
	
	var testLatLng = new GLatLng(parseFloat(this.mLatLng.lat()), parseFloat(this.mLatLng.lng()));
	this.mMarker = new GMarker(testLatLng/*this.mLatLng*/);
	
	this.setMarkerDescription = function(description) {
		this.mMarkerHTML = description;
		this.showMarker();
		this.mMarker.openInfoWindowHtml(this.mMarkerHTML);
	}
	
	// Default parameter 1: show = true
	this.showMarker = function() {
		var show = defaultParameter(arguments, 1, true);
		var iconInfo = defaultParameter(arguments, 2, null);

		if(iconInfo != null) {
			this.mIcon = iconInfo;
	
			if(this.mMarker != null) {
				this.mMap.removeOverlay(this.mMarker);
				this.mMarker = null;
			}

			var markerOptions = {icon : iconInfo};
			this.mMarker = new GMarker(this.mLatLng, markerOptions);
			this.mMarker.bindInfoWindowHtml(this.mMarkerHTML);
		}
		
		// Could just assign the default parameter to this.mIsVisible, but
		// I like the formality of the default parameter; consistent with all
		// functions that have default parameters.
		this.mIsVisible = show;
		if(this.mIsVisible) {
			this.mMarker.bindInfoWindowHtml(this.mMarkerHTML);
			this.mMap.addOverlay(this.mMarker);
		} else {
			this.mMap.removeOverlay(this.mMarker);
		}
	}

	// Just update the marker display depending on this route point's state
	this.updateMarkerDisplay = function() {
		this.showMarker(this.mIsVisible);
	}
	
	this.urlencode = function(myIndex) {
		var ret =
			'&rp' + myIndex + 'Lat=' + this.mLatLng.lat() +
			'&rp' + myIndex + 'Lng=' + this.mLatLng.lng() +
			'&rp' + myIndex + 'HTML=' + encodeURIComponent(this.mMarkerHTML) +
			'&rp' + myIndex + 'Vis=' + boolToInt(this.mIsVisible) +
			'&rp' + myIndex + 'WP=' + boolToInt(this.mIsWaypoint) +
			'&rp' + myIndex + 'RLP=' + boolToInt(this.mIsRouteLinePoint) +
			'&rp' + myIndex + 'Click=' + boolToInt(this.mIsFromClick);
			
		return ret;
	}
}

function cV4x4Route(inMap) {
	// My JS formality: ctor always defined; we call it at the bottom so
	// any functions we call from within the ctor are defined when we need
	// them. Note a difference here between JS 'ctor' and C++ ctor: in C++
	// the object doesn't even really exist until you return from the ctor.
	this.cV4x4Route = function(inMap) {
		// My JS formality: create all class members in the ctor
		this.mMap = inMap;
		this.mRoute = null;
		this.mRouteLine = null;
		this.mMarkers = null;
		this.mCurrentMarkerIndex = -1;

		this.restartInternals();
	}
	
	// Note default parameters
	this.addMarker = function(latLng, infoWindowOpenCallback, infoWindowCloseCallback) {
		var icon = defaultParameter(arguments, 4, new cIcon(3, 'blue-dot.png', false));
		var markerHTML = defaultParameter(arguments, 5, '');
		var announce = defaultParameter(arguments, 6, true);
		
		var newMarker = new cMarker(this.mMap, latLng, infoWindowOpenCallback, infoWindowCloseCallback,
									icon, markerHTML, announce);
									
		this.mMarkers.push(newMarker);
		this.mCurrentMarkerIndex = this.mMarkers.length - 1;
		return newMarker;
	}
	
	this.clearCurrentMarker = function() {
		this.mCurrentMarkerIndex = -1;
	}
	
	this.discardMarkers = function() {
		while(this.getMarkerCount() > 0) {
			this.mMarkers[this.getTopMarkerIndex()].destruct();
			this.mMarkers.pop();
		}
	}
	
	this.discardRoute = function() {
		while(this.getVertexCount())
			this.popPoint(false);

		this.discardMarkers();
		this.renewRouteLine();
	}
	
	this.extendRouteLine = function() {
		var vertexCount = theRoute.getVertexCount();

		if(vertexCount == 2)	{
			// We just now have two points, so create the polyline
			this.renewRouteLine();
		} else if(vertexCount > 2) {
			// We already had created the polyline; add a segment to it
			var topPoint = this.getTop();
			this.mRouteLine.insertVertex(vertexCount, topPoint.mLatLng);
			topPoint.showMarker(topPoint.mIsVisible);
		}
	}
	
	this.fuzzilyFindMarkerAtLatLng = function(inLatLng) {
		// If I'm doing the math right, this epsilon amounts to 18 feet at the very worst.
		// 25000 miles / 360 degrees * .00005
		var epsilon = defaultParameter(arguments, 2, 0.00005);

		for(var i in this.mMarkers) {
			if(fuzzilyEqual(inLatLng, this.mMarkers[i].mLatLng, epsilon))
				return i;
		}
		
		return -1;
	}
	
	this.fuzzilyFindLatLng = function(inLatLng) {
		// If I'm doing the math right, this epsilon amounts to 18 feet at the very worst.
		// 25000 miles / 360 degrees * .00005
		var epsilon = defaultParameter(arguments, 1, 0.00005);

		for(var i in this.mRoute) {
			if(fuzzilyEqual(inLatLng, this.mRoute[i].mLatLng, epsilon))
				return i;
		}
		
		return -1;
	}
	
	this.getCurrentMarker = function() {
		if((this.mCurrentMarkerIndex == -1) || (this.mCurrentMarkerIndex >= this.mMarkers.length))
			return false;
		else
			return this.mMarkers[this.mCurrentMarkerIndex];
	}
	
	this.getCurrentMarkerIndex = function() {
		return this.mCurrentMarkerIndex;
	}
	
	// Returns the route length in meters; returns -1 if there is no route defined yet (fewer than two points)
	this.getLength = function() {
		// Allow caller to specify the start and end points:
		// getLength([start, end]);
		var start = defaultParameter(arguments, 1, 0);
		var end = defaultParameter(arguments, 2, (this.mRoute.length - 1));
		
		if(this.mRoute.length > 0) {
			var routeLength = 0;
			var i = (start + 1);
			do {
				routeLength += this.mRoute[i].mLatLng.distanceFrom(this.mRoute[i - 1].mLatLng);
				++i;
			} while(i <= end);
			
			return routeLength;
		} else {
			return -1;
		}
	}
	
	this.getMarkerCount = function() {
		return this.mMarkers.length;
	}
	
	this.getRouteLinePoints = function() {
		var ret = Array();

		for(i in this.mRoute) {
			if(this.mRoute[i].mIsRouteLinePoint)
				ret.push(this.mRoute[i].mLatLng);
		}
		
		return ret;
	}
	
	this.getRoutePoint = function(index) {
		if((index == -1) || (index > this.getTopIndex())) {
			return false;
		} else {
			return this.mRoute[index];
		}
	}
	
	this.getTop = function() {
		return this.getRoutePoint(this.getTopIndex());
	}
	
	this.getTopIndex = function() {
		return this.getVertexCount() - 1;
	}
	
	this.getTopMarkerIndex = function() {
		return this.getMarkerCount() - 1;
	}
	
	this.getVertexCount = function() {
		return this.mRoute.length;
	}
	
	this.haveCurrentMarker = function() {
		return this.getCurrentMarkerIndex() != -1;
	}
	
	this.haveMarkerFuzzilyAt = function(latLng) {
		for(var i in this.mMarkers) {
			if(fuzzilyEqual(latLng, this.mMarkers[i].mLatLng))
				return true;
		}
		
		return false;
	}

	this.insertRoutePoint = function() {
		var latLng = defaultParameter(arguments, 1, null);
		var markerHTML = defaultParameter(arguments, 2, '');
		var isVisible = defaultParameter(arguments, 3, true);
		var isWaypoint = defaultParameter(arguments, 4, true);
		var isRouteLinePoint = defaultParameter(arguments, 5, true);
		var isFromClick = defaultParameter(arguments, 6, true);
		var extendRouteLine = defaultParameter(arguments, 7, true);
		
		this.mRoute.push(new cRoutePoint(this.mMap, latLng, markerHTML, isVisible, isWaypoint, isRouteLinePoint, isFromClick));
		
		if(extendRouteLine) {
			this.extendRouteLine();
		}
		
		this.getTop().updateMarkerDisplay();	// Make sure that the marker is showing even if we didn't draw the line
	}
	
	// Note the default parameter
	this.popPoint = function() {
		var renewRouteLine = defaultParameter(arguments, 1, true);

		var pointToPop = this.getTop();
		if(pointToPop.mIsVisible)
			this.mMap.removeOverlay(pointToPop.mMarker);

		var retval = this.mRoute.pop();
		
		if(renewRouteLine)
			this.renewRouteLine();
	}
	
	this.removeMarker = function(whichMarker) {
		this.mMarkers[whichMarker].destruct();
		this.mMarkers.splice(whichMarker, 1);
	}
	
	this.renewRouteLine = function() {
		if(this.mRouteLine != false) {
			this.mMap.removeOverlay(this.mRouteLine);
		}

		if(this.getVertexCount() >= 2)	{
			var polylineEncoder = new PolylineEncoder();
			this.mRouteLine = polylineEncoder.dpEncodeToGPolyline(this.getRouteLinePoints(), '#FF0000');
			//this.mRouteLine = new GPolyline(this.getRouteLinePoints());
			this.mMap.addOverlay(this.mRouteLine);
		}
	}
	
	this.restartAll = function() {
		// Do this before restartInternals(), because that function resets mRouteLine.
		// I'm conflicted about this idea of restartRoute() not removing the route line
		// from the map, but (1) I'm afraid to change it at this point and (2) maybe it's
		// ok to have the internal stuff reset separately from the display stuff.
		if(this.mRouteLine != false) {
			this.mMap.removeOverlay(this.mRouteLine);
		}
		
		this.discardMarkers();	// Remove them from the display (more than that, but that's what I'm after)
		this.restartInternals();
	}
	
	this.restartInternals = function() {
		this.restartRoute();
		this.restartMarkers();
	}
	
	this.restartMarkers = function() {
		this.mMarkers = new Array();
		this.mCurrentMarkerIndex = -1;
	}
	
	this.restartRoute = function() {
		this.mRoute = new Array();
		this.mRouteLine = false;
	}
	
	this.setLLBounds = function(llBounds) {
		for(var i in this.mRoute)
			llBounds.extend(this.mRoute[i].mLatLng);
	}

	// Note default parameter
	this.setMarkerCurrent = function(whichMarker) {
		var show = defaultParameter(arguments, 2, true);

		this.mCurrentMarkerIndex = whichMarker;
		
		var currentMarker = this.getCurrentMarker();
//		currentMarker.show(show);
		return currentMarker;
	}
	
	this.setTopMarkerCurrent = function() {
		this.mCurrentMarkerIndex = this.getTopMarkerIndex();
		if(this.mCurrentMarkerIndex >= 0) {
//			this.getCurrentMarker().show();
		}
	}
	
	this.urlencode = function() {
		var ret = '&count=' + this.getVertexCount();
		
		for(var i in this.mRoute) {
			ret += this.mRoute[i].urlencode(i);
		}
		
		ret += '&markercount=' + this.getMarkerCount();
		
		for(var i in this.mMarkers) {
			ret += this.mMarkers[i].urlencode(i);
		}
		
		return ret;
	}
	
	this.cV4x4Route(inMap);
}

function hidePopup()
{
	$(".popup").css({"visibility": "hidden"});
}
