//Author:       Steve Eidemiller
//Date:         05/31/2010
//Description:  Integrate the coupon scroller with the Google Map

//NOTE: Many of these functions rely on JavaScript's "function closure" to retain calling parameters for use in
//event handlers and callback functions. Each instance of the function preserves the contents of a different
//instance of the calling parameters, which are needed later when the event/callback triggers.

/*********************************************************************************************************************
Revision History:
00/00/2010 - Who - What
*********************************************************************************************************************/



//Debugging support
function xCouponMap_Debug(message)
{
	/*
	var debug = $('#debug');
	if (debug)
	{
		var html = debug.html();
		if (html.length) html += '<br/>';
		debug.html(html + message);
	}
	*/
}

//Class definition
function xCouponMapClass(mapContainerID, userLatitude, userLongitude)
{
	//Class properties
	this.mapContainerID = mapContainerID; //The DOM element ID for the map as a jQuery selector string (i.e. "#xGoogleMap")
	this.userLatitude   = userLatitude;
	this.userLongitude  = userLongitude;
	this.googleMap = null;
	this.googleMapOverview = null;
	this.geocoder = null;
	this.couponScrollerHandler = null; //Instance of a "xCouponScrollerClass" class
	this.uniqueAddresses = []; //Array of objects with .address and .marker properties
	this.maxResolveAttempts = 5;
	this.lastCouponIndex = null;

	//Return the GMap2 object
	this.getGMap2 = function()
	{
		return this.googleMap;
	};

	//Locate the specified address within the unique address queue
	this.getUniqueAddressIndex = function(address)
	{
		//For (each unique address in the queue)...
		for (var i = 0; i < this.uniqueAddresses.length; i++)
		{
			//If (the queued address matches the specified adress)...
			if (String(this.uniqueAddresses[i].address).valueOf() == String(address).valueOf())
			{
				//Return the queue index
				return i;
			}
		}

		//Not found
		return -1;
	};

	//Attempt to extract a 5-digit zip code from the end of an address that won't resolve. The idea is to use the zip code's lat/lng
	//as a map marker for the address, in place of an actual "good" address.
	this.extractZipCodeFromAddress = function(address)
	{
		address = $.trim(String(address));
		var zip = address.search(/\d{5}$/); //5 digits at the end of the address string
		if (zip != -1)
		{
			return address.substr(zip).valueOf();
		}
		var i = this.getUniqueAddressIndex(address);
		if (i > -1)
		{
			if (this.uniqueAddresses[i].defaultZip != '')
			{
				return this.uniqueAddresses[i].defaultZip;
			}
		}
		return ''; //Not found
	};

	//Resolve all the coupon addresses to lat/long points on the map by adding all unique addresses to a queue and processing them one-by-one
	this.addCoupons = function(couponScrollerHandler)
	{
		//Set a reference to this class instance within the scroller class instance
		couponScrollerHandler.mapHandler = this;

		//
		this.couponScrollerHandler = couponScrollerHandler;
		var coupons = this.couponScrollerHandler.coupons;

		//For (each coupon)...
		for (var i = 0; i < coupons.length; i++)
		{
			//If (this is a non-empty unique address for a coupon that is NOT nationwide)...
			if ($.trim(String(coupons[i].address)).valueOf() != '' && this.getUniqueAddressIndex(coupons[i].address) == -1 && coupons[i].is_nationwide == 0)
			{
				//Push an address object onto the uniqueAddresses[] queue
				this.uniqueAddresses.push(
				{
					'address'      : coupons[i].address,
					'marker'       : null, //This will be a GMarker object created upon address resolution (or will remain NULL for bad addresses)
					'latitude'     : coupons[i].latitude,
					'longitude'    : coupons[i].longitude,
					'isBadAddress' : coupons[i].is_bad_address,
					'defaultZip'   : coupons[i].account_default_zip_code,
					'isZipCode'    : false, //This is a "real" non-zip code address
					'attempts'     : 0
				});
			}
		}

		//Start looking up addresses, one after the other in somewhat rapid succession, without overloading Google's service (going faster than they want us to)
		if (this.uniqueAddresses.length)
		{
			this.resolveNextAddress();
		}
	};

	//Find the next unresolved address in the queue and resolve it asynchronously. For previously resolved addresses, use the cached lat/lng points.
	this.resolveNextAddress = function()
	{
		var bQueued = false;

		//Lookup the next unique address
		for (var attempt = 1; attempt <= this.maxResolveAttempts && !bQueued; attempt++)
		{
			for (var i = 0; i < this.uniqueAddresses.length; i++)
			{
				var address = this.uniqueAddresses[i];
				if (address.marker == null)
				{
					if (address.isBadAddress == '0' && address.latitude != 0 && address.longitude != 0)
					{
						//Use the cached lat/lng to generate a GMarker for the map
						var point = new GLatLng(address.latitude, address.longitude);
						this.createAddressGMarker(point, address.address);
					}
					else if (address.isBadAddress == '1' && address.defaultZip != '' && address.latitude != 0 && address.longitude != 0)
					{
						//Use the lat/lng of the default zip code that was found for the "bad address"
						//TODO: is this right? It's the same code as above!
						var point = new GLatLng(address.latitude, address.longitude);
						this.createAddressGMarker(point, address.address);
					}
					else if (address.isBadAddress == '1')
					{
						//Just ignore this, it's a known bad address with no way to get a lat/lng
					}
					else if (address.attempts < attempt)
					{
						//Asynchronous resolution using Google's resolver service
						this.resolveAddressToGMapPoint(address.address);
						bQueued = true;
						break;
					}
				}
			}
		}

		//If (there were no more addresses to lookup)...
		if (!bQueued)
		{
			//TODO: niCacheResolvedAddresses();
		}
	};

	//Asynchronously resolve one address to a lat/lng point on the Google Map, attempting multiple times if necessary
	//NOTE: This relies on "function closure"
	this.resolveAddressToGMapPoint = function(address)
	{
		//Update the number of lookup attempts for this address
		var addressIndex = this.getUniqueAddressIndex(address);
		this.uniqueAddresses[addressIndex].attempts++;
		var classInstance = this; //For function closure in the nested event below

		//Lookup the address with GClientGeocoder.getLatLng()
		this.geocoder.getLatLng(
			address,
			function(point)
			{
				//Callback function
				classInstance.googleGeocoderCallback.call(classInstance, point, address); //Set "this" to the current class instance with .call()
			}
		);
	};

	//Callback handler for GClientGeocoder.getLatLng()
	this.googleGeocoderCallback = function(point, address)
	{
		var addressIndex = this.getUniqueAddressIndex(address);
		if (point != null)
		{
			this.createAddressGMarker(point, address);
			xCouponMap_Debug('<span style="font-weight: bold; color: green;">FOUND:</span> ' + address + ' [' + this.uniqueAddresses[addressIndex].attempts + ' attempts, lat = ' + point.lat() + ', lng = ' + point.lng() + ']');
		}
		else if (this.uniqueAddresses[addressIndex].attempts >= this.maxResolveAttempts)
		{
			var zip = this.extractZipCodeFromAddress(address);
			if (zip.length > 0 && zip != address)
			{
				if (this.getUniqueAddressIndex(zip) == -1)
				{
					//Push this zip code onto the uniqueAddresses[] queue along with a GMarker
					this.uniqueAddresses.push(
					{
						'address'      : zip,
						'marker'       : null,
						'latitude'     : '',
						'longitude'    : '',
						'isBadAddress' : '',
						'defaultZip'   : '',   //This is a "fake" address, so don't give it a default zip code, even though it *is* a zip code already (other logic in here might not like that)
						'isZipCode'    : true, //This is a generated "fake" zip code address
						'attempts'     : 0
					}); //Create an address object for the zip code ONLY, which also stores its GMarker once it's resolved
				}

				//
				address += ' [zip = ' + zip + ']';
			}
			xCouponMap_Debug('<span style="font-weight: bold; color: red;">NOT FOUND:</span> ' + address + ' [default zip: ' + this.uniqueAddresses[addressIndex].defaultZip + ']');
		}

		//Lookup the next address
		var classInstance = this; //For function closure in the nested event below
		window.setTimeout
		(
			function()
			{
				classInstance.resolveNextAddress.call(classInstance);
			},
			50
		);
	}

	//Create a GMarker for the specified address and set up the event handler for that marker
	//NOTE: This relies on "function closure"
	this.createAddressGMarker = function(point, address)
	{
		//For function closure in the nested event below
		var classInstance = this;

		//Create the GMarker and its listener
		var marker = new GMarker(point);
		GEvent.addListener(marker, 'click', function()
		{
			//Popup the InfoWindow when the address marker is clicked on
			classInstance.showAddressInfoWindow.call(classInstance, marker, address)
		});

		//Add the marker to the map
		this.googleMap.addOverlay(marker);

		//Store this marker in the list of unique addresses so it can be referenced later
		var i = this.getUniqueAddressIndex(address);
		if (i > -1)
		{
			this.uniqueAddresses[i].marker = marker;
		}

		//Pass the new marker back to the calling function
		return marker;
	};

	//Show the Google Map's InfoWindow for the specified address. This happens when the user clicks the address marker, or when the timer pans to the address.
	this.showAddressInfoWindow = function(marker, address)
	{
		marker.openInfoWindowHtml('<div id="savingsDetail">' + this.generateCouponInfoWindowHTML(address) + '</div>');
	};

	//There can be multiple coupons at any given address, so add their information all together for the Google Map InfoWindow
	this.generateCouponInfoWindowHTML = function(address)
	{
		var locations = []; //This will be a list of objects with .coupons[] and .location properties
		var coupons = this.couponScrollerHandler.coupons;

		//For (each coupon)...
		for (var i = 0; i < coupons.length; i++)
		{
			//If (the coupon's address matches the current point)...
			if (coupons[i].address.valueOf() == address.valueOf())
			{
				//For (each different company/business already found at this address)...
				var location    = coupons[i].company;
				var couponImage = coupons[i].image;
				for (var j = 0; j < locations.length; j++)
				{
					//If (the company/business of a previous coupon is the same as this one AND the coupon image is the same)...
					if (locations[j].location == location && locations[j].couponImage == couponImage)
					{
						//Append this coupon to the list of coupons at that business/company
						locations[j].coupons.push('<a href="#" class="xDescription" language="JavaScript" onclick="xShowCouponPopup(' + coupons[i].id + '); return false;">' +coupons[i].description + '</a>');
						break;
					}
				}

				//If (the company/business for this coupon was NOT already in the list of locations at this address)...
				if (j == locations.length)
				{
					//Append a new company/business to the list of locations, along with its first coupon
					locations.push(
					{
						'coupons'     : ['<a href="#" class="xDescription" language="JavaScript" onclick="xShowCouponPopup(' + coupons[i].id + '); return false;">' + coupons[i].description + '</a>'],
						'location'    : location,
						'couponImage' : couponImage
					});
				}
			}
		}

		//For (each company/business location at this address)...
		var html = '';
		for (var i = 0; i < locations.length; i++)
		{
			var locationHTML = '';

			//If (there is just one coupon at this location)...
			var locationCoupons = locations[i].coupons;
			if (locationCoupons.length == 1)
			{
				//Simple listing
				locationHTML += '<div class="xInfoWindow xInfoWindowDeal"><nobr>' + locationCoupons[0] + '</nobr></div>';
			}
			else
			{
				//For (each coupon at this location)...
				locationHTML += '<ul style="margin: 0px 0px 0px 16px; padding: 0px;">'; //margin-top: 0px; margin-right: 0px; margin-bottom: 0px;
				for (var j = 0; j < locationCoupons.length; j++)
				{
					//Bulleted listing
					locationHTML += '<li class="xInfoWindow xInfoWindowDeal" style="margin: 0px; padding: 0px;"><nobr>' + locationCoupons[j] + '</nobr></li>';
				}
				locationHTML += '</ul>';
			}

			//Add the name of the business/location for the above list of coupons
			locationHTML += '<div class="xInfoWindow">' + locations[i].location + '</div>';

			//Modify the coupon <img> tags
			var imageHTML = locations[i].couponImage;
			imageHTML = imageHTML.replace(/([&\?])width\=\d+/gi, '$1width=32'); //Fix the dynamic image widths to 32 pixels
			imageHTML = imageHTML.replace(/\swidth\="\d+"/gi, ' width="32"'); //Fix the <img> tag widths to 32 pixels

			//Format the output
			html += '<table cellspacing="0" cellpadding="0" border="0"><tbody><tr><td width="32">' + imageHTML + '</td><td width="5">&nbsp;</td><td>' + locationHTML + '</td></tr></tbody></table>';
		}

		//Add the address for these locations
		var margin = (locations.length == 1 ? '' : ' style="margin-top: 15px;"'); //Add some extra margin before the address if there were multiple locations at this address
		html += '<div' + margin + '>' + $.trim(address.replace(/\\r/g, ' ').replace(/\\n/g, ' ')) + '</div>'; //Cleanup the address, since it contains exact information needed on the server side to implement temporary caching

		//Return the generated HTML
		return html;
	};

	//Event fired from the scroller class when the scroller moves, either by the timer or by user interaction
	this.panMap = function(couponIndex)
	{
		if (this.lastCouponIndex != couponIndex)
		{
			this.lastCouponIndex = couponIndex;

			//Pan/scroll the Google Map to the current coupon's address, and activate its InfoWindow
			try
			{
				//Get the information for the "current" address
				var coupon = this.couponScrollerHandler.coupons[this.lastCouponIndex];
				var i = this.getUniqueAddressIndex(coupon.address);
				var address = '';
				var marker = null;

				//If (the current address is in the addresses queue)...
				if (i > -1)
				{
					//If (the current address does NOT resolve to a point on the map)...
					address = this.uniqueAddresses[i].address;
					marker  = this.uniqueAddresses[i].marker;
					if (marker == null)
					{
						//If (a zip code can be extracted from the address string)...
						var zip = this.extractZipCodeFromAddress(coupon.address);
						if (zip != '')
						{
							//If (the zip code exists in the addresses queue)...
							i = this.getUniqueAddressIndex(zip);
							if (i > -1)
							{
								//Use the zip code "point" for the current address's position
								marker  = this.uniqueAddresses[i].marker;
							}
						}
					}
				}

				//If (the current address is valid and can be represented by a point on the map)...
				if (address != '' && marker != null)
				{
					this.googleMap.closeInfoWindow();
					this.googleMap.panTo(marker.getLatLng());
					this.showAddressInfoWindow(marker, address);
				}
				else
				{
					this.googleMap.closeInfoWindow();
				}
			}
			catch (e) {}
		}
	};

	//Create an instance of a Google Map
	this.initialize = function()
	{
		if (GBrowserIsCompatible())
		{
			//Display the map, with some controls
			this.googleMap = new GMap2($(this.mapContainerID).get(0));
			this.geocoder = new GClientGeocoder();
			this.googleMap.addControl(new GLargeMapControl());
			this.googleMap.addControl(new GMapTypeControl());

			//Add an overview
			this.googleMapOverview = new GOverviewMapControl(new GSize(150,150));
			this.googleMap.addControl(this.googleMapOverview);

			//Set the initial location
			this.googleMap.setCenter(new GLatLng(this.userLatitude, this.userLongitude), 12);

			//Enable some popular features
			this.googleMap.enableContinuousZoom();
			this.googleMap.enableScrollWheelZoom();
		}
		else
		{
			//Display a warning if the browser was not compatible
			$(this.mapContainerID).html('<div class="center" style="margin-top: 20px;">Sorry, the Google Maps API is not compatible with this browser</div>');
		}
	};

	//Initialize the class instance
	this.initialize();



	////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	// Address lat/lng caching support (AJAX)
	////////////////////////////////////////////////////////////////////////////////////////////////////////////////



	//Update a temporary cache of resolved address lat/lng points on the server
	this.cacheResolvedAddresses = function()
	{
		/*
		//Determine if there are any updates to pass back to the server
		var cacheUpdates = [];
		for (var i = 0; i < this.uniqueAddresses.length; i++)
		{
			//If (the address was not already cached)...
			var address = this.uniqueAddresses[i];
			if (address.isBadAddress == '')
			{
				//If (the address was successfully resolved)...
				if (address.marker != null)
				{
					//Cache the lat/lng
					var point = address.marker.getLatLng();
					cacheUpdates.push([address.address, point.lat(), point.lng(), '0']);
				}
				else
				{
					//Cache the bad address (and set lat/lng to zeroes)
					cacheUpdates.push([address.address, 0, 0, '1']);
				}
			}
		}

		//If (updates exist)...
		if (cacheUpdates.length)
		{
			//Cache them on the server temporarily
			xAjax(
			{
				'url'   : '/includes/ajax-map-server.php', //URL of the AJAX data server
				'xData' :
				{
					'ajaxCommand' : 'updateGoogleMapCache',
					'updates'     : cacheUpdates
				},
				'onSuccess' : function (jsonData, textStatus, XMLHttpRequest)
				{
					//Do nothing
					xCouponMap_Debug('<span style="font-weight: bold; color: blue;">AJAX DONE:</span> ' + jsonData.message);
				}
			});
		}
		*/
	};
}

