
/* Copyright 2010 The Firebox.
 * All rights reserved.
 * Written by Nicholas De Cicco.
 */

var newsList;

/* The number of shown news items.
 */
var numNewsItems = 0;

/* The amount of time to wait before changing to the next news item.
 */
var itemChangeDelayTime = 5000;

/* The number of pixels the box has to move to reach the next item.
 */
var moveAmount;

/* The index of the news item to which the box is scrolling.
 */
var currentItemIndex = 1;

/* Converts a date string to a Date object. If endOfDay is true, sets the time
 * of day on the date given by the string to one millisecond before midnight of
 * the next day (i.e., 23 hours, 59 minutes, 59 seconds, and 999 milliseconds);
 * otherwise, sets the time to midnight.
 */
function parseDate(dateAsString, endOfDay)
{
	/* We could use the Date.parse() method if the date were in the format
	 * Mon, 04 Oct 2010 13:30:00 GMT-0430, but it's not, so we can't. Our
	 * dates are in the format month/day/year, so instead we use the split()
	 * method of String to yield the individual elements and parse those.
	 */
	var substrings = dateAsString.split("/");

	/* If there are fewer than three elements in the date, fill in the
	 * remaining elements with zeros. If there are more than three, ignore
	 * them.
	 */
	if (substrings.length != 3) {
		logError("parseDate(): error: invalid number of date components");
		if (substrings.length < 3) {
			for (var i = substrings.length; i < 3; i++) {
				substrings[i] = 0;
			}
		}
	}

	resetIfNaN = function(value) {
		if (isNaN(value)) {
			return 0;
		} else {
			return value;
		}
	};

	/* Note that months start at 0:
	 */
	var year = resetIfNaN(parseInt(substrings[2], 10));
	var month = resetIfNaN(parseInt(substrings[0], 10))-1;
	var day = resetIfNaN(parseInt(substrings[1], 10));

	if (endOfDay) {
		return new Date(year, month, day, 23, 59, 59, 999);
	} else {
	return new Date(year, month, day, 0, 0, 0, 0);
	}
}

function NewsList(listName)
{
	this.numNewsItems = 0;

	this.parseList = function(xmlDocument) {

		/* Get the current time/date in EST.
		 * FIXME: do we have to subtract four hours or do anything like that?
		 */
		var now;
		if ("now" in Date) {
			now = Date.now();
		} else {
			now = new Date();
		}

		/* Get a handle on the news box on the main page. This is where news
		 * content will be added.
		 */
		var newsBox = document.getElementById("newsbox");
		if (newsBox === null) {
			logError("NewsList.parseList(): fatal: no news box found!");
			return;
		}

		/* Get an array of the news items in the xml file. The contents of
		 * these will be parsed and added to the page, provided that the local
		 * user's system date is set appropriately; only current items will be
		 * shown.
		 */
		var newsItems = xmlDocument.getElementsByTagName("item");

		/* Validate that there are actually news items on the page. If there
		 * are not, abort silently. FIXME: do we check for null or length?
		 */
		if (newsItems.length < 1) {
			logError("NewsList.parseList(): fatal: no news items found!");
			return;
		}

		/* Here we define a method to ensure that we cannot accidentally
		 * attempt to parse a missing node; if the node is missing, this
		 * method will return an empty string. The reason is that if we
		 * attempt to parse a non-existant node, JavaScript will get angry.
		 */
		var getNodeValue = function(node) {
			try { return node.childNodes[0].nodeValue; }
			catch (e) { return ""; }
		};

		/* Parse the individual news items, adding them provided that they
		 * are valid and their start and end dates are effective.
		 */
		for (var i = 0; i < newsItems.length; i++)
		{
			var startDate, endDate, header, content;

			/* Parse the item header (title).
			 */
			titleElements = newsItems[i].getElementsByTagName("title");
			if (titleElements.length == 1) {
				header = document.createElement("h1");
				header.innerHTML = getNodeValue(titleElements[0]);
				if (header.innerHTML === "") {
					logError("NewsList.parseList(): no header found for " +
						"item, skipping...");
					continue;
				}
			} else {
				logError("NewsList.parseList(): zero or more than one " +
					"header elements found for item, skipping...");
			}

			/* Parse the item content.
			 */
			contentElements = newsItems[i].getElementsByTagName("description");
			if (contentElements.length == 1) {
				content = document.createElement("p");
				content.innerHTML = getNodeValue(contentElements[0]);
				if (content.innerHTML === "") {
					logError("NewsList.parseList(): error: no content " +
						"found for item, skipping...");
					continue;
				}
			} else {
				logError("NewsList.parseList(): zero or more than one " +
					"content elements found for item, skipping...");
			}

			/* Parse the start date.
			 */
			startDateElements = newsItems[i].getElementsByTagName("startDate");
			if (startDateElements.length == 1) {
				startDate = getNodeValue(startDateElements[0]);
				if (startDate === "") {
					logError("NewsList.parseList(): no start date found " +
						"for item, skipping...");
					continue;
				}
			} else {
				logError("NewsList.parseList(): zero or more than one " +
					"start date elements found for item, skipping...");
			}

			/* Parse the end date.
			 */
			endDateElements = newsItems[i].getElementsByTagName("endDate");
			if (endDateElements.length == 1) {
				endDate = getNodeValue(endDateElements[0]);
				if (endDate === "") {
					logError("NewsList.parseList(): error: no end date " +
						"found for item, skipping...");
					continue;
				}
			} else {
				logError("NewsList.parseList(): error: zero or more than " +
					"one end date elements found for item, skipping...");
			}

			/* Only add the item if the current date falls between the start
			 * and end dates for the item.
			 */
			if ((parseDate(startDate, false).getTime() < now) &&
				(now < parseDate(endDate, true).getTime()))
			{
				numNewsItems++;

				newsBox.appendChild(header);
				newsBox.appendChild(content);

				var links = newsItems[i].getElementsByTagName("link");
				if (links.length == 1) {
					if (getNodeValue(links[0]) !== "") {
						var link = document.createElement("a");
						link.setAttribute("href", getNodeValue(links[0]));
						link.setAttribute("target", "_blank");
						link.innerHTML = "Read more...";
						newsBox.appendChild(link);
					}
				}
			}
		}
		if (numNewsItems > 1) {
			window.setTimeout(startMoveNewsBox, itemChangeDelayTime);
		}
	};

	/* Downloads the news list from the local machine asynchronously,
	 * parsing the list and adding it to the page when it finishes
	 * downloading.
	 */
	this.fetchList = function(listName) {
		var documentRequest =
			new DocumentRequest(listName, this.parseList);
	};

	/* Load the requested sheet.
	 */
	this.fetchList(listName);
}

/* Calculates the amount that the box has to move to reach the next news item
 * and begins the animation, so long as there are more elements to display;
 * when the last is reached, it is scrolled out of view and the list is
 * scrolled back into sight from the bottom of the parent box upward.
 */
function startMoveNewsBox()
{
	var newsBox = document.getElementById("newsbox");
	if (!newsBox) { return; }

	var element = newsBox.firstChild;
	if (!element) { return; }

	if (currentItemIndex == 0) {
		currentItemIndex++;
		var parentBox = document.getElementById("parentbox");
		moveAmount = 0;
		newsBox.style.top = String(parentBox.offsetHeight) + "px";
	} else if (currentItemIndex < numNewsItems) {
		currentItemIndex++;
		/* Calculate the (absolute) distance that the news box must travel to
		 * place the next news item at the top.
		 */
		var i = 0;
		moveAmount = 0;
		while (1) {
			if (element.tagName.toLowerCase() == "h1") { i++; }
			if (!(element = element.nextSibling)) {
				logError("Less elements present than the current item index!");
				return;
			}
			if (i < currentItemIndex) {
				var margins = getMargins(element);
				moveAmount += element.offsetHeight + margins[0] + margins[2];
			} else { break; }
		}
	} else if (currentItemIndex == numNewsItems) {
		/* When we've reached the last item in the list, scroll it out of view
		 * and set the current item index to zero; this indicates that we must
		 * then, after the animation completes, scroll the news box back into
		 * view.
		 */
		currentItemIndex = 0;
		var margins = getMargins(newsBox);
		moveAmount = newsBox.offsetHeight + margins[0] + margins[2];
	}
	window.setTimeout(moveNewsBox, 0);
}

/* Moves the news box and sets a timeout for this same function to continue
 * doing so until it reaches the offset given by 'moveAmount'.
 */
function moveNewsBox()
{
	var newsBox = document.getElementById("newsbox");
	var topOffset = parseInt(getStyle(newsBox, "top"), 10);

	/* Move the box until it has reached the next news item.
	 */
	if ((Math.abs(topOffset) < moveAmount && moveAmount > 0) ||
		(topOffset > 0 && moveAmount == 0)) {
		newsBox.style.top = String(topOffset - 1) + "px";
		window.setTimeout(moveNewsBox, 33);
	} else {
		window.setTimeout(startMoveNewsBox, itemChangeDelayTime);
	}
}

/* Initializes the page, loading the news list, setting the height of the
 * "parent box", and beginning the scrolling animation.
 */
function initialize()
{
	/* Hopefully cross platform hack to get around the cache */
	newsList = new NewsList("news.xml?" + (new Date()).getTime());

	/* Set the height of the parent box such that it fits nicely in its parent
	 * node. For this, we set the height of the parent box to that of its
	 * parent, minus its margin. Getting the top- and bottom- margins of an
	 * element is not so straight-forward, however:
	 */
	var parentBox = document.getElementById("parentbox");
	if (!parentBox) { return; }

	var margins = getMargins(parentBox);
	if (!margins) { return; }

	parentBox.style.height = String(parentBox.parentNode.clientHeight -
		margins[0] - margins[2]) + "px";

	/* For some odd reason, Internet Explorer does not appear to set position
	 * styles (i.e., "top", "right", "bottom", "left") initially; they must be
	 * changed for them to be able to be parsed. Therefore, to ensure proper
	 * functioning of startMoveNewsBox and all that, we must initialize the
	 * position at zero (which it would be normally).
	 */
	document.getElementById("newsbox").style.top = "0px";

	if (document.domain == "") {
		document.getElementById("errorLog").style.display = "block";
	}
}

/* Add the initialization function to the queue of functions to run on load.
 */
if ("addEventListener" in window) {
	/* Firefox et al. */
	window.addEventListener("load", initialize, true);
} else {
	/* Internet Explorer */
	attachEvent("onload", initialize);
}

