//
// EinsteinDirectory core javascript
//
// Author: 	Brian Smith
// Date: 	August 2007
//

EI.namespace("Directory");

EI.Directory = function(mapElement, options) {

	var _options = options || {}

	var map = new EI.Directory.Map(mapElement, _options.centerLatLng, _options.radius);
	var self = this;
	
	// Loads the map and runs any necessary initialization. Should be set as a window (or document) load event.
	//
	this.load = function() {
		self.beforeLoad();
		
		if(map) {
			map.load();
		}
		
		self.afterLoad();
	};
	
	// Unloads the map and runs any necessary teardown code. Should be set as a window (or document) unload event.
	//
	this.unload = function() {
		self.beforeUnload();
		
		if(map) {
			map.unload();
		}
		
		self.beforeUnload();
	};
	 
	// Add an entity
	//
	// options: 
	//	newEntity - JSON object of an entity
	// 	newEntityHtml - HTML representation of entity
	// 	newEntityLinkClass - HTML class for links to this entity
	//
	this.addEntity = function(newEntity, options) {
		var options = options || {}
		
		self.beforeAddEntity(newEntity, options);
				
		if(map) {
			map.addEntity([newEntity.location.latitude, newEntity.location.longitude], options);
		}
		
		self.afterAddEntity(newEntity, options);
	};
	
	// Remove all entities
	//
	//
	this.clearEntities = function() {
		self.beforeClearEntities();
		
		if(map) {
			map.clearEntities();
		}
		
		self.afterClearEntities();
	};
	
	// Set the center and radius of the search.
	//
	// newLatLng needs to be a two-element array with the 
	// latitude and longitude of strings.
	//
	// newRadius needs to be an integer (for radius of search in miles)
	//
	this.setCenter = function(newLatLng, newRadius) {
		self.beforeSetCenter(newLatLng, newRadius);
		
		if(map) {
			map.moveTo(newLatLng, newRadius);
		}
		
		self.afterSetCenter(newLatLng, newRadius);
	};
	
}
// Before and after callbacks
//
// You can overwrite any of these callbacks to allow for custom
// functionality.
//
// Example:
//
// 	EI.Directory.prototype.afterLoad = function() { alert("Loaded!"); };
//
EI.Directory.prototype.afterUpdate = function() {};
EI.Directory.prototype.beforeUpdate = function() {};
EI.Directory.prototype.afterAddEntity = function(newEntity, newEntityHtml, newEntityLinkClass) {};
EI.Directory.prototype.beforeAddEntity = function(newEntity, newEntityHtml, newEntityLinkClass) {};
EI.Directory.prototype.afterLoad = function() {};
EI.Directory.prototype.beforeLoad = function() {};
EI.Directory.prototype.afterUnload = function() {};
EI.Directory.prototype.beforeUnload = function() {};
EI.Directory.prototype.afterClearEntities = function() {};
EI.Directory.prototype.beforeClearEntities = function() {};
EI.Directory.prototype.afterSetCenter = function(newLatLng, newRadius) {};
EI.Directory.prototype.beforeSetCenter = function(newLatLng, newRadius) {};


// The Map constructor creates a new map.
//
// Requires: Prototype 1.5+
//
// Parameters:
//
//	mapElement: 		The element (or string id of an element) to use for displaying the map,
//									most likely a div element.
//	centerLatLng: 	GLatLng object
//	radius: 				The minimum radius (in miles) that the map should cover.
// 
EI.Directory.Map = function (mapElement, centerLatLng, radius) {
	
	this.defaultCenter = new GLatLng(39.9, -98.5);
	this.defaultRadius = 1400;
	
	var _mapElement = $(mapElement);
	var _centerLatLng = centerLatLng || this.defaultCenter;
	var _radius = radius || this.defaultRadius;
	
	var _map = null;
	var _entityMarkers = new Array;
	
	var self = this;

	function centerLat() {
		return parseFloat(_centerLatLng.lat());
	}
	
	function centerLng() {
		return parseFloat(_centerLatLng.lng());
	}

	function radiusToZoom() {
		degLatInMiles =  69.170323428;
		degLngInMiles = degLatInMiles * Math.cos(centerLat() * 0.0174532925199433);
				
		diffDegLat = _radius / degLatInMiles;
		diffDegLng = _radius / degLngInMiles;
				
		swBound = new GLatLng(centerLat() - diffDegLat, centerLng() - diffDegLng);
		neBound = new GLatLng(centerLat() + diffDegLat, centerLng() + diffDegLng);

		return _map.getBoundsZoomLevel(new GLatLngBounds(swBound, neBound));
	}
	
	function loadEntityMarkers() {
		for( var i = 0; i < _entityMarkers.length; i++) {
			_map.addOverlay( _entityMarkers[i] );
		}
	}
	
	function addLinkInfoEvents(entityIndex, linkClass) {
		linkElements = $$('a.' + linkClass);
		for(var i = 0; i < linkElements.length; i++) {
			
		}
	}
	
	
	this.load = function() {
		if (GBrowserIsCompatible()) {
			_map = new GMap2(_mapElement);

			_map.addControl(new GSmallMapControl());
			// _map.addControl(new GMapTypeControl());

			_map.setCenter(new GLatLng(centerLat(), centerLng()), radiusToZoom());

			loadEntityMarkers();
			return true;			
		}
		return false;
	};
	
	this.unload = function() {
		if (GBrowserIsCompatible()) {
			GUnload();
			return true;
		}
		return false;
	};
	
	this.addEntity = function(markerLatLng, options) {

		var options = options || {};
		var markerOptions = {};

		var icon = new GIcon(options.icon || G_DEFAULT_ICON);
		markerOptions.icon = icon;

		var newPoint = new GLatLng(markerLatLng[0], markerLatLng[1]);
		var newMarker = new GMarker(newPoint, markerOptions);

		// Attach behavior to update custom infoWindow element with entityHTML
		if (options.customInfoWindowElement) {

			GEvent.addListener(newMarker, 'click', function() {
				$(options.customInfoWindowElement).update(options.entityHTML);

				if (typeof Effect != 'undefined') {
					new Effect.Highlight(options.customInfoWindowElement, { 
						keepBackgroundImage: true, 
						startcolor: '#cfe8ff',
						endcolor: '#ffffff',
						restorecolor: true
					});
				}
			});

		// Otherwise, default behavior
		} else {
			newMarker.bindInfoWindowHtml(options.entityHTML);
		}

		GEvent.addListener(newMarker, 'mouseover', function() {
			newMarker.setImage(icon.imageOver);
		});

		GEvent.addListener(newMarker, 'mouseout', function() {
			newMarker.setImage(icon.image)
		});

		// Attach click event trigger to classed links
		entityLinks = $$('a.' + options.entityLinkClass);
		for(var i = 0; i < entityLinks.length; i++) {
			Element.observe(entityLinks[i], "click", function() { GEvent.trigger(newMarker, "click"); });
		}

		_entityMarkers.push(newMarker);

		if( _map ) {
			_map.addOverlay(newMarker);
		}
		
		return true;
	};
	
	this.clearEntities = function () {
		var currentMarker;
		
		while(currentMarker = _entityMarkers.pop()) {
			_map.removeOverlay(currentMarker);
		}
		return true;
	};
	
	this.moveTo = function(newLatLng, newRadius) {
		_radius = newRadius;
		_centerLatLng = newLatLng;
		
		_map.setZoom(radiusToZoom());
		_map.panTo( new GLatLng(centerLat(), centerLng()));
		
		return true;
	};

};


// Geo.GeoLocation core JavaScript
// 

EI.namespace('Geo');

// Encapsulates location information returned from geocoding request.
// 
// Parameters:
// 
//  loc:    Object with the following properties: streetAddress, city, state, postalCode, countryCode, provider
//
EI.Geo.GeoLocation = function(loc) {

	var self = this;

	var _loc = loc || {};

	this.latLng        = new GLatLng(_loc.latitude, _loc.longitude);
	this.latitude      = this.latLng.lat()  || null;
	this.longitude     = this.latLng.lng()  || null;
	this.streetAddress = _loc.streetAddress || null;
	this.city          = _loc.city          || null;
	this.state         = _loc.state         || null;
	this.postalCode    = _loc.postalCode    || null;
	this.countryCode   = _loc.countryCode   || null;
	this.provider      = _loc.provider      || null;
	this.accuracy      = _loc.accuracy      || 10;
	this.maxAccuracy   = 100;

	// Returns whether this geocoding request successfully recovered a geographic point
	// 
	this.isSuccess = function() {
		return (this.latitude != null && this.longitude != null);
	};

	// Returns whether this geolocation is located in the US
	// 
	this.isUS = function(){
		return /^US$/i.match(this.countryCode);
	};

	// Returns all non-empty geolocation information in single-line 
	// string for easy use in other geocoding services
	// 
	this.toGeocodableString = function(){
		var a = [this.streetAddress, this.city, this.state, this.postalCode, this.countryCode];
		for (i = 0; i < a.length; i++) {
			a[i] ? true : delete a[i];
		}
		return a.compact().join(', ');
	};

	// Returns all geocoding information in string
	// 
	this.toString = function(){
		return "Provider: " + this.provider + "\nStreet Address: " + this.streetAddress + "\nCity: " + this.city + "\nState: " + this.state + "\nPostal Code: " + this.postalCode + "\nLatitude: " + this.latitude + "\nLongitude: " + this.longitude + "\nCountry: " + this.countryCode + "\nSuccess: " + this.isSuccess();
	};

};


//
// Geo.IpGeocoder core javascript
//

EI.namespace('Geo');

// The IpGeocoder constructor appends a script tag that loads the following MaxMind API functions:
// 
//  - geoip_country_code()
//  - geoip_country_name()
//  - geoip_city()
//  - geoip_region()
//  - geoip_latitude()
//  - geoip_longitude()
// 
// Requires:  Prototype	1.6+, MaxMind GeoIP Javascript Web Service
// 
// Parameters:
// 
//  ipAddress:     The client IP address for geocoding
//  onComplete:    Callback to be called upon completion of Ajax request
//
EI.Geo.IpGeocoder = function() {

	var self        = this;

	var _apiUrl     = 'http://j.maxmind.com/app/geoip.js';
	var _periodExec = null;
	var _timeoutId  = null;

	this.geoLocation = null;

	// Query MaxMind and set geoLocation on success
	//
	this.findAddress = function(address, onComplete) {

		var scriptId  = EI.Util.generateRandomElementId();
		var scriptSrc = _apiUrl;

		// Append script tag to call remote service (circumvents cross-domain policy)
		EI.Util.appendScriptElement(scriptSrc, scriptId);

		// Once all MaxMind GeoIP functions are loaded, parse & map their values, then complete
		_periodExec = new PeriodicalExecuter(function(pe) {
			if ( geoIpFunctionsExist() ) {
				pe.stop();
				self.geoLocation = parseResponse();
				if (typeof(onComplete) == 'function')
					onComplete(self.geoLocation);
			}
		}, 0.2);

		// To prevent this from endlessly executing, set a timeout of 2 seconds
		_timeoutId = window.setTimeout(function() {
			if (_periodExec.currentlyExecuting) {
				_periodExec.stop(); 
				if (typeof(onComplete) == 'function')
					onComplete(self.geoLocation);	
			}
		}, 2000);

	};

	// Test for existence of MaxMind API functions
	// 
	function geoIpFunctionsExist() {
		return ( typeof geoip_country_code != 'undefined' &&
		         typeof geoip_country_name != 'undefined' &&
		         typeof geoip_city != 'undefined' &&
		         typeof geoip_region != 'undefined' &&
		         typeof geoip_latitude != 'undefined' &&
		         typeof geoip_longitude != 'undefined'
		);
	}

	// Maps MaxMind function values
	// Returns object of EI.Geo.GeoLocation
	// 
	function parseResponse() {
		var loc = {
			'provider':     'MaxMind',
			'latitude':     geoip_latitude(),
			'longitude':    geoip_longitude(),
			'countryCode':  geoip_country_code(),
			'state':        geoip_region(),
			'city':         geoip_city()
		};
		return new EI.Geo.GeoLocation(loc);
	}

};

// Geo.GoogleGeocoder core JavaScript
// 
// The GoogleGeocoder constructor returns an object for Google geocoding service
// 
// Requires: EI.Geo.GeoLocation, 	Google Maps API
//
EI.Geo.GoogleGeocoder = function() {

	var self            = this;
	var _geocoder       = new GClientGeocoder();

	this.geoLocation = null;

	// Query Google geocoder with address string. Delegate geolocation response to onComplete.
	// 
	this.findAddress = function(address, onComplete) {
		_geocoder.getLocations(address, function(response) {
			var loc = parseResponse(response);
			if (typeof(onComplete) == 'function') 
				onComplete(loc);
		});
	};

	// Response parsed as xAL (eXtensible Address Language). 
	// TODO: (refactor) this is a bit of a mess, due to the unpredictable node hierarchy
	// 
	function parseResponse(response) {

		if (!response || response.Status.code != 200)
			return false;

		var place       = response.Placemark[0];
		var loc         = {};
		var area        = null;
		var locality    = null;

		loc.provider    = 'Google';

		if (place) {

			loc.latitude    = place.Point.coordinates[1];
			loc.longitude   = place.Point.coordinates[0];
			loc.accuracy    = accuracyToRadius(place.AddressDetails.Accuracy);

			if (place.AddressDetails.Country) {

				loc.countryCode = place.AddressDetails.Country.CountryNameCode;

				if (place.AddressDetails.Country.AdministrativeArea) {

					// For some reason, Google adds '.' to state names, for example C.A. for California
					if (place.AddressDetails.Country.AdministrativeArea.AdministrativeAreaName)
						loc.state = place.AddressDetails.Country.AdministrativeArea.AdministrativeAreaName.replace(/\./g, '');

					// Assign the proprerty node from which we will pull Locality information
					area = place.AddressDetails.Country.AdministrativeArea.SubAdministrativeArea || 
					       place.AddressDetails.Country.AdministrativeArea;

					if (area) {
						if (area.Locality) {
							// Ignore DependentLocalityName (borrough/community) and just use name of city
							loc.city = area.Locality.DependentLocality ? 
								area.SubAdministrativeAreaName : 
								area.Locality.LocalityName;

							if (area.Locality.PostalCode)
								loc.postalCode = area.Locality.PostalCode.PostalCodeNumber;

							if (area.Locality.Thoroughfare)
								loc.streetAddress = area.Locality.Thoroughfare.ThoroughfareName;	
						}
					}
				}
			}
		}
		return self.geoLocation = new EI.Geo.GeoLocation(loc);
	}

	// Translates Google geocoding accuracy value to radius
	// 
	function accuracyToRadius(accuracy) {
		var accuracy = parseFloat(accuracy);
		var defaultRadius = 1500;
		var radius = {
			0: defaultRadius,  // Unknown
			1: 400,            // Country
			2: 200,            // Region (state/province)
			3: 10,             // Sub-region (county/municipality) <-- buggy, 3 and 4 are sometimes reversed
			4: 10,             // Town (city/village)
			5: 5,              // Post code
			6: 2,              // Street
			7: 1,              // Intersection
			8: 1               // Address
		};
		return radius[accuracy] || defaultRadius;
	}

};

//
// GeoListings core javascript
//

EI.namespace('Directory.GeoListings');

// The GeoListings constructor creates an IpGeocoder object, then initializes map
// 
// Requires: EI.Geo.IpGeocoder, EI.Cookie
// 
// Parameters:
// 
//  mapElement:     The element (or string id of an element) to use for displaying the map,
//                  most likely a div element.
//  options
//    address:                  Client address for geocoding
//    specialtyId:              Specialty ID for listing API call
//    callback:                 Name of function with which to wrap JSON listing results.  This is a user-defined
//                              function, assumed to be present.  The callback function receives JSON results 
//                              as a parameter and is called automatically upon completion of API call. 
//    customInfoWindowElement:  (optional) Element (or string id of element) for updating listing infoWindow HTML
//    loadingIndicatorElement:  (optional) Element (or string id of element) assigned 'loading' class during geocoding
//
EI.Directory.GeoListings = function(mapElement, options) {

	var self = this;

	var _options             = options || {};
	var _apiUrl              = 'http://www.lawyershop.com/cgi-opt/geolist.cgi';
	var _cookie              = new EI.Cookie('_ei_geolistings'); // FIXME: bring Cookie under EI namespace?
	var _defaultSearchRadius = 100;
	var _defaultZoom         = 10;
	var _defaultLatLng       = new GLatLng(39.9, -98.5);
	var _geoLocation         = null;
	var _geocoder            = null;
	var _mapElement          = mapElement;
	var _systemMessageDiv    = document.createElement('div');

	var _icons               = [
		// Priority
		{
			image:             _options.imagePath + '/geo_listings_marker_priority.png',
			imageOver:         _options.imagePath + '/geo_listings_marker_priority_over.png',
			shadow:            _options.imagePath + '/geo_listings_marker_priority_shadow.png',
			iconSize:          new GSize(22, 39),  // FIXME: tightly-coupled to GSize & GPoint. Push off to EI.Directory.Map?
			shadowSize:        new GSize(42, 39),
			iconAnchor:        new GPoint(11, 39),
			infoWindowAnchor:  new GPoint(10, 2)
		},
		// Featured
		{
			image:             _options.imagePath + '/geo_listings_marker_featured.png',
			imageOver:         _options.imagePath + '/geo_listings_marker_featured_over.png',
			shadow:            _options.imagePath + '/geo_listings_marker_featured_shadow.png',
			iconSize:          new GSize(20, 34),
			shadowSize:        new GSize(38, 34),
			iconAnchor:        new GPoint(9, 34),
			infoWindowAnchor:  new GPoint(9, 2)
		},
		// Profiled (Basic)
		{
			image:             _options.imagePath + '/geo_listings_marker_featured.png',
			imageOver:         _options.imagePath + '/geo_listings_marker_featured_over.png',
			shadow:            _options.imagePath + '/geo_listings_marker_featured_shadow.png',
			iconSize:          new GSize(20, 34),
			shadowSize:        new GSize(38, 34),
			iconAnchor:        new GPoint(9, 34),
			infoWindowAnchor:  new GPoint(9, 2)
		}
	];

	this.directory     = null;

	// Geocodes free-text address and updates geoLocation.
	// On completion, updates listings.
	// 
	this.updateLocation = function(address, radius) {

		if (!/^\s*$/.test(address)) {

			$(options.loadingIndicatorElement).addClassName('loading');

			_geocoder.findAddress(address, function(geoLoc) {

				if (geoLoc && geoLoc.isSuccess()) {

					// Update map with new geolocation and listings if accuracy is better than state-level
					if (geoLoc.accuracy < geoLoc.maxAccuracy) {
						self.directory.setCenter(geoLoc.latLng, geoLoc.accuracy);
						self.directory.clearEntities();
						getListings(geoLoc.latLng, radius);

						// Cache last-searched location
						self.setGeoLocation(geoLoc);
						updateCookie();

					// Otherwise, display notice
					} else {
						displaySystemMessage('Please narrow your search');
					}
				} else {
					displaySystemMessage('Unable to find location');
				}

				$(options.loadingIndicatorElement).removeClassName('loading');
			});
		}
	};

	// Add entity to directory map.  Pass along matching icon object & options.
	// 
	this.displayListing = function(listing, options) {
		options.icon = _icons[(listing.listingType-1)];
		if (_options.customInfoWindowElement)
			options.customInfoWindowElement = _options.customInfoWindowElement;
		self.directory.addEntity(listing, options);
	};

	// Sets _geoLocation property
	// 
	this.setGeoLocation = function(geoLocation) {
		_geoLocation = geoLocation;
	};

	// Initialize map, either at geocoded location or default (U.S.)
	// 
	function setupDirectoryMap() {

		var radius = _geoLocation && _geoLocation.isSuccess() ? _geoLocation.accuracy : 1500;
		var latLng = _geoLocation && _geoLocation.isSuccess() ? _geoLocation.latLng   : _defaultLatLng;

		_geocoder = new EI.Geo.GoogleGeocoder();

		self.directory = new EI.Directory(mapElement, { centerLatLng: latLng, radius: radius });

		if (self.directory) {
			Event.observe(window, 'unload', self.directory.unload);
			self.directory.load();
			getListings(latLng);
		}

		// Update cookie geoLocation
		updateCookie();

		return false;
	}

	// Get listings from EIS, based on location and specialty ID
	// 
	function getListings(latLng, radius) {

		var params = {
			latitude:     latLng.lat(),
			longitude:    latLng.lng(),
			radius:       radius || _defaultSearchRadius,
			specialty_id: _options.specialtyId,
			callback:     _options.callback
		};

		var scriptId  = EI.Util.generateRandomElementId();
		var scriptSrc = _apiUrl  + '?' + $H(params).toQueryString();
		
		// Append script tag to call remote service (circumvents cross-domain policy
		EI.Util.appendScriptElement(scriptSrc, scriptId);
	}

	// Takes all properties of geoLocation and copies them to Cookie object
	// 
	function updateCookie() {
		for (property in _geoLocation) {
			_cookie[property] = _geoLocation[property];
		}
		_cookie.store(999,'/');
	}

	// Updates and displays system message overlay element
	// 
	function displaySystemMessage(message) {
		var systemMessageDiv = _systemMessageDiv || document.createElement('div');
		systemMessageDiv.setAttribute('id', 'geo_listings_message');

		$(systemMessageDiv).update('<p>' + message +'</p>');

		$(systemMessageDiv).setStyle({
			textAlign:          'center',
			position:           'absolute',
			width:              '100%',
			top:                '40%',
			zIndex:             99999
		});
		$(systemMessageDiv).down('p').setStyle({
			display:            'block',
			width:              '165px',
			border:             '3px solid #cc0000',
			padding:            '7px',
			margin:             '0 auto',
			textAlign:          'center',
			backgroundColor:    '#ffffff'
		});

		// Append system message and set timeout to remove
		$(mapElement).appendChild(systemMessageDiv);
		setTimeout(function(){ $(mapElement).removeChild($(systemMessageDiv)); }, 3000);
	}

	// Initialize map; constructor should be called on dom ready
	// Use cookie geolocation if exists, otherwise IP detection or use address if provided
	// 
	var initialize = function() {

		_geocoder = _options.address ? new EI.Geo.GoogleGeocoder : new EI.Geo.IpGeocoder;

		if (_cookie.latitude && _cookie.longitude && _options.address == null	) {
			self.setGeoLocation( new EI.Geo.GeoLocation(_cookie) );
			setupDirectoryMap();
		} else {
			_geocoder.findAddress(_options.address, function(geoLoc){
				self.setGeoLocation(geoLoc);
				setupDirectoryMap();
			});
		}
	}();

};


//
// Util core javascript
//

EI.namespace("Util");

// This namespace is for commonly utility functions used across multiple applications. 

EI.Util = function() {

	var self = this;

	// Called automatically by anonymous timeout function set by appendScriptElement.
	// Removes script tag from document <head>
	// 
	function removeScriptElement(id) {
		if (headElem = document.getElementsByTagName('head')[0]) headElem.removeChild($(id));
	}

	return {

		// Creates script tag and appends to document <head>
		// 
		appendScriptElement: function(src, id) {
			var scriptElem = document.createElement('script');
			scriptElem.setAttribute('type', 'text/javascript');
			scriptElem.setAttribute('charset', 'UTF-8');
			scriptElem.setAttribute('id', id)
			scriptElem.setAttribute('src', src);
			// Adds script tag and set timeout to remove after 5 seconds
			if (headElem = document.getElementsByTagName('head')[0]) {
				headElem.appendChild(scriptElem);
				window.setTimeout(function(){ removeScriptElement($(id)) }, 5000);
			}
			else return false;
		},

		// Generates a random valid ID for usage with DOM elements
		// 
		generateRandomElementId: function() {
			return '_EI-' + (new Date).getTime().toString(36);
		}

	}
}();
