/* Place the code into a Tides namespace. This function will export just those
 * methods that should be publically exposed, including:
 *   
 *   fetch
 *   data
 *   
 * Of these, only fetch is important to clients. The data property is exposed
 * for implementation reasons. */

var Tides = new function()
{
	
	/* ID attribute of the HTML element into which we'll inject the widget. */
	
	var _elementID = null;
	
	/* Tide or current location. */
	
	var _locationID = 0;
	
	/* Event options. */
	
	var _events = {
		"sun" : false,
		"moon" : false,
		"twilight" : false,
		"moonPhases" : false
	};
	
	/* The number of days to display tide or current information for. */
	
	var _days = 1;
	
	/* Function to call when the render process completes. */
	
	var _callback = null;
	
	/* Number of times we've checked to see if the data struct has been
	 * returned. */
	
	var _lookupAttempts = 0;
	 
	/* Limits the number of times we check for the data structure. 100 at 10 per
	 * second equals 10 seconds. */
	
	var _maxLookupAttempts = 100;
	
	/* Controls how long we wait in between checks to see if the data structure
	 * has been returned. */
	  
	var _interval = 100; // milliseconds
	
	/* HTML fragments used to build the widget.
 	 * 
	 * NOTE: This code uses a technically illegal JavaScript syntax, escaping
 	 * line breaks with slashes. However, this syntax is supported in all major
	 * browsers. */
	
	var _templates = {
		"shell" : '\
		<div class="cape-tides">\
			<table>\
				<thead>\
					<tr>\
						<th class="location" colspan="3">\
							${location.name}\
							<div class="coordinates">\
								${location.latitude}, ${location.longitude}\
							</div>\
						</th>\
					</tr>\
				</thead>\
				${dates}\
				<tfoot>\
					<tr>\
						<td class="tag-line" colspan="3">\
							${tagline}\
						</td>\
					</tr>\
				</tfoot>\
			</table>\
		</div>\
		',
		"date" : '\
		<tbody>\
			<tr>\
				<th class="date" colspan="3">\
					${date}\
				</th>\
			</tr>\
			${events}\
		</tbody>\
		',
		"event" : '\
		<tr>\
			<td class="time">\
				${time}\
			</td>\
			<td class="event">\
				${name} ${data}\
			</td>\
		</tr>\
		'
	};
	
	/* Constructs a URL to the to the data, appending a timestamp to avoid HTTP
	 * request caching. */

	function getDataUrl()
	{
		var url;
		
		var append = new function()
		{
			return function(name, value)
			{
				if (!url)
				{
					url = getBaseUrl();
					url += _appRootRelativeDataPath;
					url += "?"
				}
				else
				{
					url += "&";
				}
				
				url += name + "=" + value;
			};
		};
		
		append("locationID", _locationID);
		append("days", _days);
		
		if (_events.sun)       { append("showSun", "true");      }
		if (_events.twilight)  { append("showTwilight", "true"); }
		if (_events.moon)      { append("showMoonrise", "true"); }
		if (_events.moonPhase) { append("showMoon", "true");     }
		
		url += "#";
		url += getSerializedTimeStamp();
		
		return url;
	}
	
	/* Application root relative path to the file containing the current, JSON
	 * serialized data. */
	
	var _appRootRelativeDataPath = "widget/tides.cfm";
	
	/* Returns the URL the application root directory. The path ends in a
	 * trailing slash. */
	
	function getBaseUrl()
	{
		var currentScriptPath = getCurrentScriptPath();
		
		var baseUrlLength = currentScriptPath.length - _appRootRelativeScriptPath.length
		
		var baseUrl = currentScriptPath.substring(0, baseUrlLength);
		
		return baseUrl;
	}
	
	/* Returns the absolute URL of this script. */
	
	function getCurrentScriptPath()
	{
		var scripts = document.getElementsByTagName("SCRIPT");
		
		for (var i in scripts)
		{
			var script = scripts[i];
			
			if (isCurrentScriptPath(script.src))
			{
				return script.src;
			}
		}
		
		throw("Failed to determine current script path.");
	}
	
	function isCurrentScriptPath(path)
	{
		// Check for undefined. Also picks up a zero length string.
		
		if(!path)
		{
			return false;
		}
		
		// Make sure the path is long enough to be a match.
		
		var scriptPathLength = _appRootRelativeScriptPath.length;
		
		if (path.length < scriptPathLength)
		{
			return false;
		}
		
		// Check to see if the right portion of the path matches the app root
		// relative path for this script.
		
		var start = path.length - scriptPathLength;
		
		var portionOfPathToCheck = path.substring(start);
		
		if (portionOfPathToCheck == _appRootRelativeScriptPath)
		{
			return true;
		}
		
		// The path did not match. This is not our script path.
		
		return false;
	}
	
	/* App root relative path to this JavaScript file. Used to reconstruct the
	 * URL to to the application root. */
	
	var _appRootRelativeScriptPath = "widget/tides.js";
	
	/* Returns the current date/time as a serial (numeric) value. Can be used to
 	 * timestamp a request, bypassing caching for HTTP requests. */
	
	function getSerializedTimeStamp()
	{
		var ts = new Date();
		
 		return ts.valueOf();
	}

	/* Fetches the data from the server. The data is serialized in a JSON data
	 * structure. It will be eval'd into the Tides.data member. */
	
	function loadData()
	{
		var scriptElement = document.createElement("script");
		
		scriptElement.setAttribute("type", "text/javascript");
		
	  	scriptElement.setAttribute("language", "JavaScript");
		
		var dataUrl = getDataUrl();
		
		scriptElement.setAttribute("src", dataUrl);
		
		document.getElementsByTagName("head")[0].appendChild(scriptElement);
	}
	
	/* Returns a reference to the container element into which the widget code
	 * will be injected. */
	
	function getContainer()
	{
		return document.getElementById(_elementID);
	}
	
	/* Returns true if the data, container element, and template elment
	 * (assuming there is one) have all been loaded. If so, then we can begin
	 * rendering the weather. */
	
	function isLoaded()
	{
		if (Tides.data == null)
		{
			return false;
		}
		
		if (getContainer() == null)
		{
			return false;
		}
		
		return true;
	}
	
	/* Injects the widget into to the predetermined element (usually a div).
	 * This function loops, defering execution until all dependencies have been
	 * loaded. Once the data been rendered, the callback function is executed
	 * (if it has been defined). */
	
	function render()
	{
		_lookupAttempts++;
		
		if (!isLoaded())
		{
			if (_lookupAttempts <= _maxLookupAttempts)
			{
				window.setTimeout(function() { render() }, _interval);
			}
			else
			{
				window.status = "Failed to load tides after "
						+ _maxLookupAttempts + " attempts.";
			}
			
			return;
		}
		
		// The container element is the HTML element into which we'll render the
		// data.
		
		var container = getContainer();
		
		var content = generateWidget();
		
		container.innerHTML = content;
		
		// We've finsihed updating the template. If we have a callback function,
		// call it now.
		
		if (_callback != null)
		{
			_callback();
		}
	}
	
	function generateWidget()
	{
		// FIXME: we should probably copy the data since we're modifying it.
		
		var data = Tides.data;
		
		data.dates = generateWidgetDates();
		
		var template = _templates["shell"];
		
		var content = interpolate(template, data);
		
		return content;
	}
	
	function generateWidgetDates()
	{
		var content = "";
		var template = _templates["date"];
		
		var eventCollections = getEventsByDate();
		
		for (var i = 0; i < eventCollections.length; i++)
		{
			var eventCollection = eventCollections[i];
			
			var eventContent = generateWidgetEvents(eventCollection.events);
			
			var dateContent = interpolate(
					template,
					{
						"date" : eventCollection.date,
						"events" : eventContent
					}
			);
			
			content += dateContent;
		}
		
		return content;
	}
	
	function generateWidgetEvents(events)
	{
		var content = "";
		var template = _templates["event"];
		
		for (var i = 0; i < events.length; i++)
		{
			var event = events[i];
			
			var eventContent = interpolate(template, event);
			
			content += eventContent;
		}
		
		return content;
	}
	
	/* Loops through an array of events ordered by date. Returns a new array of
	 * objects. Each object contains the events for a given day. The individual
	 * objects look like this:
	 *   
	 *   { "date" : date_as_string, "events" : array_of_events }
	 */
	
	function getEventsByDate()
	{
		var eventCollections = [];
		var currentDate = "";
		
		for (var i = 0; i < Tides.data.events.length; i++)
		{
			var event = Tides.data.events[i];
			
			var eventCollection;
			
			if (currentDate != event.date)
			{
				currentDate = event.date;
				
				eventCollection = { "date" : event.date, "events" : [] }
				
				eventCollections.push(eventCollection)
			}
			else
			{
				eventCollection = eventCollections[eventCollections.length - 1];
			}
			
			eventCollection.events.push(event);
		}
		
		return eventCollections;
	}
	
	/* Replaces all occurences of ${key} in a string where key corresponds
	 * to an element in the values collection. */
	 
	function interpolate(string, values)
	{
		/* The regular expression finds all occurences of ${word} where word
		 * is any word. It also includes occurences of $%7Bword%7D, which is
		 * the URL escape sequence for the former. If ${word} is embedded in
		 * an img src attribute, Firefox will escape it to the latter. */
		
		return string.replace(
			/\$(\{|%7B)[\w\.]+(\}|%7D)/gi,
			function(str, p1, offset, s)
			{
				// str is our match. It will look something like ${word}.
				// We'll strip off the ${} (or $%7B%7D).
				
				var key = tokenToIdentifier(str);
				
				// Retrieve the value which corresponds to this key.
				
				var value = getValue(values, key);
				
				return value;
			}
		);
	}
	
	/* Takes a key name (myKey) or path (myObject.myKey) and returns the
	 * corresponding element from the data. */
	
	function getValue(data, key)
	{
		var elements = key.split(".");
		
		for (var i = 0; i < elements.length; i++)
		{
			var element = elements[i];
			
			data = data[element];
		}
		
		return data;
	}
	
	/* Takes a token that was embedded in a template and extracts the
	 * identifier. For instance, the following:
	 *   
	 *   ${IDENTIFIER}
	 *   
	 * Will return IDENTIFIER.
	 * 
	 * Note that it will also check for $%7BIDENTIFIER%7D, which is the URL
	 * escape sequence for the former. If ${IDENTIFIER} is embedded in an
	 * img src attribute, Firefox will escape it to the latter. */
	 
	function tokenToIdentifier(token)
	{
		if (token.indexOf("${") > -1)
		{
			return token.substring(2, token.length - 1);
		}
		else
		{
			return token.substring(4, token.length - 3);
		}
	}
	
	/* This is the main entry point for client applications. The only required
	 * parameter is elementID. elementID is the HTML ID attribute of the element
	 * which will contain the tides (usually a div).
	 * 
	 * Finally, if work needs to be done once the weather has been rendered, a
	 * call back function can be supplied. This should be an actual function
	 * reference, not the name of a function. */
	
	function fetch(elementID, locationID, options)
	{
		_elementID = elementID;
		
		_locationID = locationID;
		
		if (typeof options != "undefined")
		{
			if (typeof options.days != "undefined")
			{
				_days = options.days;
			}
			
			if (typeof options.callback == "function")
			{
				_callback = options.callback;
			}
			
			// Events
			
			_events.sun = options.sun ? true : false;
			_events.twilight = options.twilight ? true : false;
			_events.moon = options.moon ? true : false;
			_events.moonPhases = options.moonPhases ? true : false;
			
		}
		
		loadData();
		
		render();
	}
	
	/* Expose our public interface. We'll expose the fetch method because that
	 * is the entry point for client applications. The data property is exposed
	 * so that, when the browser loads the JavaScript file with the JSON
	 * payload, it can be passed into this object. */
	
	return {
		"fetch" : fetch,
		"data" : null
	};
};