﻿/// <reference path="jquery-1.4.1-vsdoc.js" />
/*global artifacts, jQuery, gMovementLag, gIntroTimingVariance, gCarouselItemsPerPage, gPerformanceLitmus, gImageLoadTimeout, gTooltipVerticalGap, gStrings , gSettings, gHowToMenuItems, gChromeImages, gModalAssets */

jQuery.noConflict();

/// Office chrome JavaScript
jQuery(document).ready(function () {
    var leftPos, topPos;
    /* Menu mouseover*/
    jQuery('li.cdtopn').hover(
		function() {
			leftPos = jQuery(this).position().left;
			topPos = jQuery(this).position().top;
			jQuery('ul', this).css('top', (topPos + 29) + 'px');
			jQuery('ul', this).css('left', (leftPos) + 'px');
			jQuery('ul', this).css('display', 'block');
		},
		function () { jQuery('ul', this).css('display', 'none'); }
	);
    //	watermark
	jQuery('#qu').each(function () {
	    var default_value = this.value;
	    jQuery(this).focus(function () {
	        if (this.value === default_value) {
	            this.value = '';
	        }
	    });
		jQuery(this).blur(function () {
	        if (this.value === '') {
	            this.value = default_value;
	        }
	    });
	});
});


/// Stage JavaScript

(function ($) {

	/// Reusable objects
	var $gStage, $gTooltip, $gStageCanvas;
	var $gScreen;
	var $gMouemoveContainer;
	var $gLoading, $gLoadingPercent;
	var $gBirdsEyeContainer, $gBirdsEyeDock, $gBirdsEyeMask;
    var $gEssentialsContainer, $gHowToContainer, $gAnswersContainer, $gDrawerDock;
	var $gLayer;
	var gModal = [];
	var $gCloseModal, $gModalPageNav, $gCarousel, $gThumbnail;
	var $gDownlevel;

	/// Global variables
	var gMouseX = 0, gMouseY = 0;
	var gDeltaX = 0, gDeltaY = 0;
	var gMouseInitiated = false;
	var gMouseActivated = false;
	var gCurrentLag = MOVEMENT_LAG;
	var gUpdater;
	var gCurrentHoveredArtifact = null;
	var gCurrentModal = null;
	var gModalGroup = {};
	var gModalGroupTooltips = [];
	var gStageWidth, gStageHeight, gWindowWidth;
	var gLayerWidth, gLayerHeight;
	var gCurLayerPos = [];
	var gBirdsEyeWidth, gBirdsEyeHeight;
	var gModalLoaded = {};
	var gImagesLoaded = false;
	var gFrozen = false;
	var gUseAnimations = true;
	var gIsWin7 = false;

	$(document).ready(function () {

		// Browser sniff
		if (!isSupported) { return; }

		// Certain artifacts only visible for Windows 7
		gIsWin7 = true, (navigator.userAgent.toLowerCase().indexOf("windows nt 7") > -1);
		
		// Debugging
		if (window.location.search.indexOf("toggle") > 0) {
			$("<button/>").html("ON").click(function(){toggleSteam(true);}).appendTo("#body");
			$("<button/>").html("OFF").click(function(){toggleSteam(false);}).appendTo("#body");
		}
		if (window.location.search.indexOf("disable") > 0) { gUseAnimations = false; }
		if (window.location.search.indexOf("lag") > 0) { MOVEMENT_LAG = parseInt(window.location.search.substr(5)); }

	    gWindowWidth = $(window).width();

		$gDownlevel = $("#downlevel");
		
		var requiredImages = [];

	    // Stage element
	    $gStage = $("<div/>").attr("id", "stage");
		$("#stage_container").empty().append($gStage);
	    gStageWidth = $gStage.width();
	    gStageHeight = $gStage.height();
		
		// Default mouse position is centered
		gMouseX = gStageWidth / 2;
		gMouseY = gStageHeight / 2;
		gDeltaX = gMouseX;
		gDeltaY = gMouseY;

		$gLayer = $("<div/>")
			.attr("id", "layer")
			.addClass("layer")
			.appendTo($gStage);

		$gLayer.css({ 
				"left": -parseInt((($gLayer.width() - gStageWidth) / 2), 10) + "px",
				"top": -parseInt((($gLayer.height() - gStageHeight) / 2), 10) + "px"
			});

		gLayerWidth = $gLayer.width();
		gLayerHeight = $gLayer.height();

	    // Tooltip element
	    $gTooltip = $("<div/>")
			.addClass("tooltip")
			.appendTo($gStage);

	    // Screen for modal objects
	    $gScreen = $("<div/>")
			.attr("id", "screen")
			.hide()
			.appendTo($gStage);

		// Container to capture mousemove event
	    // Capture all clicks to the stage, only act when appropriate
		$gMouemoveContainer = $("<div/>")
			.attr("id", "mousemove_container")
			.bind("mousemove", onStageMouseMove)
			.bind("mouseover", onStageMouseOver)
			.bind("mouseout", onStageMouseOut)
			.bind("click", onStageClick)
			.appendTo($gStage);


	    // Tier 1 modal
	    gModal[1] = $("<div/>")
			.attr("id", "tier1")
			.addClass("modal")
			.hide()
			.appendTo($gStage);

	    // Tier 2 modal
	    gModal[2] = $("<div/>")
			.attr("id", "tier2")
			.addClass("modal")
			.hide()
			.appendTo($gStage);
			
	    gModal[3] = $("<div/>")
			.attr("id", "tier3")
			.addClass("modal")
			.hide()
			.appendTo($gStage);

		// Navigational elements in modals
		$gCloseModal = $("<div/>")
			.addClass("close-modal")
			.append("<span/>")
			.find("span")
				.text("X")
			.end();

		$gModalPageNav = $("<div/>")
			.addClass("page-nav");

		// Bird's Eye View
		$gBirdsEyeDock = $("<div/>")
			.attr("id", "birds_eye_view_dock")
			.bind("click", toggleBirdsEyeView)
			.bind("mousemove", onDrawerEngage)
			.bind("mouseenter", onDrawerEngage)
			.bind("mouseleave", onDrawerDisengage)
			.append("<span/>")
			.find("span")
				.html(gStrings.birdsEyeDockText)
			.end()
			.append("<div/>")
			.find("div")
				.addClass("down")
			.end()
			.appendTo($gStage);
		$gBirdsEyeContainer = $("<div/>")
			.attr("id", "birds_eye_view_container")
			.addClass("drawer")
			.bind("click", onDrawerEngage)
			.bind("mousemove", onDrawerEngage)
			.bind("mouseenter", onDrawerEngage)
			.bind("mouseleave", onDrawerDisengage)
			.appendTo($gStage);
		$("<div/>")
			.attr("id", "birds_eye_view_close")
			.html("X")
			.bind("click", toggleBirdsEyeView)
			.appendTo($gBirdsEyeContainer);
		var $birdsEye = $("<div/>")
			.attr("id", "birds_eye_view")
			.appendTo($gBirdsEyeContainer);
		gBirdsEyeWidth = $birdsEye.width();
		gBirdsEyeHeight = $birdsEye.height();
		$gBirdsEyeMask = $("<canvas/>")
			.attr({ width: gBirdsEyeWidth, height: gBirdsEyeHeight })
			.appendTo($birdsEye);
		updateBirdsEyeMask();

		// Drawer Dock
		$gDrawerDock = $("<div/>")
			.appendTo($gStage)
			.attr("id", "drawer_dock")
			.bind("click", function(e) { e.stopPropagation(); })
			.bind("mousemove", onDrawerEngage)
			.bind("mouseenter", onDrawerEngage)
			.bind("mouseleave", onDrawerDisengage)
			.append("<div/>")	// add how-to div
			.find("div:last")	// select most recently added div (last .append())
				.addClass("dock-button essentials")	// class to identify howto div
				.bind("click", toggleEssentials)	// click event for howto
				.append("<div/>")	// add arrow div
				.find("div")	// select it
					.addClass("drawer-icon")
				.end()	// /div class="down"
				.append("<span/>")	// create container for text
				.find("span")	// select it
					.html(gStrings.essentialsText)
				.end()	// /span
			.end()	// /div
			.append("<div/>")
			.find("div:last")
				.addClass("dock-button howto")
				.bind("click", toggleHowTo)
				.append("<div/>")
				.find("div")
					.addClass("drawer-icon")
				.end()
				.append("<span/>")
				.find("span:last")
					.html(gStrings.howToText)
				.end()
			.end()
			.append("<div/>")
			.find("div:last")
				.addClass("dock-button answers")
				.bind("click", toggleAnswers)
				.append("<div/>")
				.find("div")
					.addClass("drawer-icon")
				.end()
				.append("<span/>")
				.find("span:last")
					.html(gStrings.answersText)
				.end()
			.end();

		// Essentials Drawer
		// Take this content directly from the chrome
		$("#ctas").hide();
		// Replace big images with smaller counterparts
		var $essentials = $("#ctalinks")
			.find("#boxshot")
				.attr("src", "img/onenote_box.png")
			.end()
			.find("#device_lockup")
				.attr("src", "img/device_lockup.png")
			.end()
			.find("#template_lockup")
				.attr("src", "img/template_lockup.png")
			.end()
			.find("#action_computer")
				.attr("src", "img/action_computer.png")
			.end();
		$gEssentialsContainer = $("<div/>")
			.appendTo($gStage)
			.attr("id", "drawer_essentials")
			.addClass("drawer list-drawer")
			.bind("click", onDrawerEngage)
			.bind("mousemove", onDrawerEngage)
			.bind("mouseenter", onDrawerEngage)
			.bind("mouseleave", onDrawerDisengage)
			.append($essentials);

		// How-To Drawer
		$gHowToContainer = $("<div/>")
			.appendTo($gStage)
			.attr("id", "drawer_howto")
			.addClass("drawer list-drawer")
			.bind("click", onDrawerEngage)
			.bind("mousemove", onDrawerEngage)
			.bind("mouseenter", onDrawerEngage)
			.bind("mouseleave", onDrawerDisengage);
		var $howToContent = $("<ul/>").appendTo($gHowToContainer);
		$.each(gHowToMenuItems, function() {
			var $a = $("<a/>")
				.attr({"href": this.link, "target": "_blank"})
				.html(this.text);
			$("<li/>").appendTo($howToContent).append($a);
		});

		// Answers drawer
		$gAnswersContainer = $("<div/>")
			.appendTo($gStage)
			.attr("id", "drawer_answers")
			.addClass("drawer list-drawer")
			.bind("click", onDrawerEngage)
			.bind("mousemove", onDrawerEngage)
			.bind("mouseenter", onDrawerEngage)
			.bind("mouseleave", onDrawerDisengage);
		var $answersContent = $("<ul/>").appendTo($gAnswersContainer);
		if (gAnswersMenuItems) {
			$.each(gAnswersMenuItems, function() {
				var $a = $("<a/>")
					.attr({"href": this.link, "target": "_blank"})
					.html(this.text);
				$("<li/>").appendTo($answersContent).append($a);
			});
		}
                   
		// "Find" form in Answers drawer
		var $findBox = $("<input/>")
			.attr({"type": "text", "placeholder": gStrings.findBoxPlaceholder});
		var $findButton = $("<button/>")
			.text(gStrings.findButton)
			.bind("click", function() {
				this.blur();
				if ($findBox.val() === $findBox.attr("placeholder") || $findBox.val() === "") { return false; }
                window.open(gStrings.findButtonUrl + $findBox.val());
			});
		$("<form/>")
			.appendTo($gAnswersContainer)
			.append($findBox, $findButton)
			.bind("submit", function(e) { e.preventDefault(); });
		var placeholder = ($findBox[0].hasOwnProperty("placeholder"));	// Does the browser support placeholder text?
		if (!placeholder) {
			$findBox
				.addClass("placeholder")
				.val(gStrings.findBoxPlaceholder)
				.bind("focus", function() {
					if ($(this).val() === gStrings.findBoxPlaceholder) { $(this).removeClass("placeholder").val(""); }
				})
				.bind("blur", function() {
					if ($(this).val() === "") { $(this).addClass("placeholder").val(gStrings.findBoxPlaceholder); }
				});
		}

		// Birds-eye-view and drawer image preloaders
		$.each(gChromeImages, function() {
			var $img = $("<img/>").attr("src", this).addClass("preloader").appendTo("body");
			requiredImages.push($img);
		});

	    // Loading animation
		var $spinner = $("<img/>").attr({ "id": "spinner", "src": "img/spinner.gif"});
		var $logo = $("<img/>").attr({ "id": "loading_logo", "src": "img/g-one_note_logo.png"});
	    $gLoadingPercent = $("<p/>").addClass("percent").html("");
	    $gLoading = $("<div/>")
			.attr("id", "loading")
			.appendTo($gStage)
			.append($spinner, $logo, $gLoadingPercent);
		positionOnStage($gLoading);

		// Add lockup to the stage
		var $lockupImg = $("<img/>")
			.attr("src", "img/stage_lockup.jpg")
			.addClass("lockup preloader")
			.appendTo($gLayer);
		requiredImages.push($lockupImg);

		// Watch image must be loaded and ready to go
		var $watchForCanvas = $("<img/>")
			.attr("src", "img/s2/watch.png")
			.addClass("preloader")
			.appendTo("body");
		requiredImages.push($watchForCanvas);

		// Windows 7 only artifact
		$.each(gSettings.windows7Artifacts, function() {
			var $img = $("<img/>")
				.attr("src", this.src)
				.addClass("preloader")
				.addClass(this.id + "-secondary")
				.appendTo($gLayer);
			this.img = $img;
			requiredImages.push($img);
		});

		// Add background img to the stage
		var $stageBackground = $("<img/>")
			.attr("src", "img/stage_background.jpg")
			.appendTo($gLayer);
		requiredImages.push($stageBackground);

		// <canvas>
		$gStageCanvas = $("<canvas/>")
			.attr({"id":"stage_canvas", "width": gSettings.animationCanvasWidth, "height": gSettings.animationCanvasHeight})
			.appendTo($gLayer);

		// Add a div to the stage for each artifact, according to its settings
		$.each(artifacts, function (i) {
			var me = this;
			
			// Certain artifacts are omitted for non-Windows 7 OSs
			if (!gIsWin7) {
				$.each(gSettings.windows7Artifacts, function() {
					if (this.id === me.id) {
						me.show = false;
						return false;
					}
				});
			}

			if (this.show) {
				$("<div/>")
					.attr("id", this.id)
					.addClass("img-artifact")
					.css({
						"top": this.coords[1] + "px",
						"left": this.coords[0] + "px",
						"width": this.dimensions[0] + "px",
						"height": this.dimensions[1] + "px",
						"z-index": this.layer
					})
					.data("props", this)
					.appendTo($gLayer);

				// Also keep track of grouped artifacts (Tiers 1 & 2 only)
				var tier = this.tier.split("-");
				if (parseInt(tier[0], 10) < 3) {
					if (!gModalGroup[tier[0] + "-" + tier[1]]) { gModalGroup[tier[0] + "-" + tier[1]] = []; }
					gModalGroup[tier[0] + "-" + tier[1]].push(this);
				}
			}
		});

		// Start the image loading timer
		// If initial image download takes longer than the threshhold, suggest the lo-fi version of the site
		gImagesLoaded = false;
		function suggestLofi() {
			if (!gImagesLoaded) {
				$gLoading.fadeOut();
				$gDownlevel
					.empty()
					.show()
					.removeClass("preloader")
					.addClass("secondary")
					.appendTo("#stage_container")
					.append("<h2>" + gStrings.downlevelHeader + "</h2>")
					.append("<p/>")
					.find("p")
						.append(gStrings.downlevelParagraphStart)
						.append("<a/>")
						.find("a")
							.attr("href", gStrings.downlevelUrl)
							.html(gStrings.downlevelLinkText)
						.end()
						.append(gStrings.downlevelParagraphEnd)
					.end();
				positionOnStage($gDownlevel);
			}
		}
		setTimeout(suggestLofi, gImageLoadTimeout * 1000);
		
		var loadedImages = [];	// keeps track of images fully loaded so far
		var ready = false;	// keeps track of the two contingencies: main image load completion and browser performance test
		$.each(requiredImages, function () {
			$(this).load(function() {
				loadedImages.push($(this).attr("src"));
				//$gLoadingPercent.text(parseInt((loadedImages.length / requiredImages.length) * 100) + "%");
			
				if (loadedImages.length === requiredImages.length) {
					// Show stage, unless browser performance test is still pending
					if (ready) { showStage(); }
					else { ready = true; }
				}
			});
		});

		var secondaryImages = [];
		function testComplete(success) {
			gUseAnimations = (gUseAnimations && success);

			if (gUseAnimations) {
				// Show stage, unless main image preloading is still pending
				if (ready) { showStage(); }
				else { ready = true; }
			} else {
				// preload backup images
				var backupImages = [];

				var $watch = $("<img/>")
					.attr("src", "img/stage_watch_secondary.png")
					.addClass("preloader watch-secondary")
					.appendTo($gLayer);
				secondaryImages.push($watch);

				var $lockup = $("<img/>")
					.attr("src", "img/stage_lockup_secondary.png")
					.addClass("preloader lockup-secondary")
					.appendTo($gLayer);
				secondaryImages.push($lockup);
				
				$.each(secondaryImages, function () {
					$(this).load(function() {
						backupImages.push($(this).attr("src"));
			
						if (backupImages.length === secondaryImages.length) {
							// Show stage, unless main image preloading is still pending
							if (ready) { showStage(); }
							else { ready = true; }
						}
					});
				});
			}
		}
		testBrowserPerformance(testComplete);

		function showStage() {
	        // Browser performance test is complete and all images have fully loaded
			gImagesLoaded = true;
			$gDownlevel.fadeOut();
	        $gLoading.fadeOut();

			if (gUseAnimations) {
				$lockupImg.fadeIn("fast", function() {
					$stageBackground.fadeIn("slow", function() {
						additionalInit();

						var frameRate = gSettings.animationFPS;

						// Performance on Firefox & Safari has proven to be too choppy
						//$.browser.safari = !($.browser.safari && /chrome/.test(navigator.userAgent.toLowerCase()));
						//if ($.browser.mozilla || $.browser.safari) frameRate = gSettings.animationLowFPS;
						if ($.browser.mozilla) { frameRate = gSettings.animationLowFPS; }

						// run the canvas with the desired frame rate
						run($gStageCanvas[0], frameRate);

						// attach a handler to the lock, will callback when all images are
						// loaded and ready to run
						listenForLockReadyStatus(runLockAnimation.bind());
					});
				});
			} else {
				// Browser failed the performance test
				// Show static lockup and watch rather than animated versions
				$.each(secondaryImages, function () {
					$(this).fadeIn("slow");
				});
				$stageBackground.fadeIn("slow", function() {
					additionalInit();
				});
			}

			function additionalInit() {
				initMouseMovement();
				$gBirdsEyeDock.animate({"top": "0"}, 300, "easeInOutQuad");
				$gDrawerDock.animate({"top": "0"}, 300, "easeInOutQuad");
				deepLink();

				if (gIsWin7) {
					$.each(gSettings.windows7Artifacts, function() {
						this.img.fadeIn("fast");
					});
				}
			}
		}

		function deepLink() {
			if (location.hash && location.hash.length > 0) {
				var hash = location.hash.substr(1);

				$.each(artifacts, function (i) {
					if (this.show) {
						// Hash is the headline of the target deep link, but hashified
						if (this.tooltip && hashify(this.tooltip) == hash) {
							gCurrentHoveredArtifact = this;
							$gMouemoveContainer.trigger("click");
							return false;
						}
					}
				});
			}
		}
	});

	function runLockAnimation() {
		// startLock(duration, x, y)
		//	- duration: how fast lock should animate, in seconds (optional, defaults to half a second)
		//	- x: the x-position where the lock should show (optional, defaults to 22px)
		//	- y: the y-position where the lock shoudl show (optional, defaults to 70px)
		startLock(0.75);
	}

	function initMouseMovement() {
	    // Mouse movement within the stage
		gMouseInitiated = true;
		enableStageClicks();

	    // Certain properties must be updated when window is resized
	    $(window).bind("resize", onWindowResize);
	}

	/// These handlers respond to mouse interactions of the stage "cover"
	function onStageMouseMove(e) {
		gMouseX = e.pageX - $gStage[0].offsetLeft;
		gMouseY = e.pageY - $gStage[0].offsetTop;
		gMouseActivated = true;
	}
	function onStageMouseOut() {
		gMouseActivated = false;
		disableStageClicks();
	}
	function onStageMouseOver() {
	    enableStageClicks();
	}

	function onWindowResize() {
	    gWindowWidth = $(window).width();
	    gStageWidth = $gStage.width();
	    gStageHeight = $gStage.height();
		updateBirdsEyeMask();
		positionOnStage($gLoading);
	}

	/// Assure that drawer interaction is distinct from stage interaction
	function onDrawerEngage(e) {
		e.stopPropagation();
		disableStageClicks();
		hideTooltip();
	}
	function onDrawerDisengage(e) {
		enableStageClicks();
	}

	/// Update the position of the layer within the viewport
	/// The addition of the delta makes for some rudimentary easing
	function updateLayerPos() {
		var oldDelta = [gDeltaX, gDeltaY];
		var newDelta = [];
		newDelta[0] = gDeltaX + ((gMouseX - gDeltaX) / gCurrentLag);
		newDelta[1] = gDeltaY + ((gMouseY - gDeltaY) / gCurrentLag);

		// Round to nearest two decimal places
		gDeltaX = Math.round(newDelta[0] * 100) / 100;
		gDeltaY = Math.round(newDelta[1] * 100) / 100;

		// a^2 + b^2 = c^2
		var variance = Math.sqrt(Math.pow(oldDelta[0] - gDeltaX, 2) + Math.pow(oldDelta[1] - gDeltaY, 2));

		// Prevent movement past a certain tolerance
		if (variance < 0.1) {
			return;
		}
			
		offsetX = -(((gLayerWidth - gStageWidth) * gDeltaX) / gStageWidth);
		offsetY = -(((gLayerHeight - gStageHeight) * gDeltaY) / gStageHeight);
		
		$gLayer.css({ 
				"left": offsetX + "px",
				"top": offsetY + "px"
			});
			
		// getLayerPos returns an array: [position (x=0, y=1)][layer number]
		var layerPos = getLayerPos();

		// No need to update if mouse is stationary
		if (gCurLayerPos && layerPos[0] === gCurLayerPos[0] && layerPos[1] === gCurLayerPos[1]) { return; }
		gCurLayerPos[0] = layerPos[0];
		gCurLayerPos[1] = layerPos[1];

		updateBirdsEyeMask(layerPos);

		if (gMouseActivated) {
			detectArtifactHover(gCurLayerPos);
		}
	}

	function disableStageClicks(fromModal) {
		gCurrentHoveredArtifact = null;
		hideTooltip();
		$("body").removeClass("clickable");

		if (fromModal) {
			clearInterval(gUpdater);
			gFrozen = true;
		}
	}

	function enableStageClicks(fromModal) {
		if (gMouseInitiated && (!gFrozen || fromModal)) {
			clearInterval(gUpdater);
			gUpdater = setInterval(updateLayerPos, gMovementRefresh);
			gFrozen = false;
		}
	}

	// Satellites are the smaller stationary tooltips for grouped artifacts
	function hideTooltip(justSatellites) {
		if (!justSatellites) { $gTooltip.css({ "width": "auto" }).hide(); }
		$.each(gModalGroupTooltips, function() {
			if (this instanceof jQuery) {
				this.fadeOut("fast");
			}
		});
	}

	/// evaluate the mouse cursor with respect to the layer to determine if it is hovered over any artifact polygon
	/// when this is the case, track the current hovered artifact and display tooltips as needed
	function detectArtifactHover(layerPos) {
		// Check each of the artifacts to see if the cursor is over it
	    var highestLayer = 0;
	    var currentHoveredArtifact = null;
	    $.each(artifacts, function () {
	        if (this.imagemap && this.show) {
				var x, y;
				
				// Adjust mouse cursor (m) to be relative to the artifact (a) in the veiwport (v)
				// Relative position = (m - v) - a
				x = (gMouseX - layerPos[0]) - this.coords[0];
				y = (gMouseY - layerPos[1]) - this.coords[1];

				// Check to see if the mouse cursor is currently within the bounds of this artifact
				if (isInShape([x, y], this.imagemap) && this.layer >= highestLayer) {
					highestLayer = this.layer;
					currentHoveredArtifact = this;
				}
	        }
	    });
	    
	    // Show the tooltip for the artifact in question, bound to the mouse cursor
		var posX = gMouseX;
		var posY = gMouseY;

	    if (currentHoveredArtifact) {
			// Turns on the "pointer" cursor
	        $("body").addClass("clickable");

			// If we need to calculate a new width, move the box offscreen to do so
			var differentArtifact = (currentHoveredArtifact !== gCurrentHoveredArtifact);
	        var boxWidth, boxHeight;
			if (differentArtifact) {
				$gTooltip.css({"left": "-10000px", "width": "auto"}).show().html(currentHoveredArtifact.tooltip);
				boxWidth = $gTooltip.outerWidth();
				boxHeight = $gTooltip.outerHeight();
				gCurrentHoveredArtifact = currentHoveredArtifact;
			} else {
				$gTooltip.html(currentHoveredArtifact.tooltip);
				boxWidth = $gTooltip.outerWidth();
				boxHeight = $gTooltip.outerHeight();
			}

	        var cursorGap = (boxWidth/2);
            var cursorGapY = gTooltipVerticalGap;
	        var xOffset, yOffset;
            var tipTriangle = "triangle";

	        // Default x position is to the right, unless box will overlap right border
	        var tooltipRightEdge = posX + cursorGap;
	        var stageRightEdge = $gStage.offset().left + gStageWidth;
            var tooltipLeftEdge = posX - cursorGap;
            var stageLeftEdge = $gStage.offset().left;

			if (stageLeftEdge >= tooltipLeftEdge) {
				xOffset = { "left": (stageLeftEdge) + "px" };
			} else {
				if (tooltipRightEdge >= stageRightEdge || tooltipRightEdge >= gWindowWidth) {
					xOffset = { "left": (stageRightEdge - boxWidth) + "px" };
				} else {
					xOffset = { "left": (posX - cursorGap) + "px" };
				}
			}

			// Default y position is to the top, unless box will overlap top border (or window border)
			var tooltipBottomEdge = posY + boxHeight;
			var stageTopEdge = cursorGapY + boxHeight;

			if (stageTopEdge >= tooltipBottomEdge) {
				yOffset = { "top": (posY + cursorGapY) + "px" };
				tipTriangle = "triangleTop";
			} else {
				yOffset = { "top": (posY - cursorGapY) + "px" };
			}

			if (!$gTooltip.is(":visible")) {
				$gTooltip.css({ "width": boxWidth +"px"});
			}

	        $gTooltip
				.show()
                .removeClass()
				.addClass("tooltip " + tipTriangle)
				.css(xOffset)
				.css(yOffset);

			// Tier 1 & 2 modals are grouped; show icons over other artifacts in the group
			var tier = currentHoveredArtifact.tier.split("-");
			if (parseInt(tier[0], 10) < 3) {

				// Assign the proper icon
				var iconClass = "t" + tier[0] + tier[1];
				$gTooltip.addClass("grouped " + iconClass);

				if (differentArtifact) {
					// Find each of the other artifacts in this group
					$.each(gModalGroup[tier[0] + "-" + tier[1]], function(i) {
						// Grouped tooltips are clones of the main tooltip
						// Always re-use an existing clone unless it has not been created yet
						if (!gModalGroupTooltips[i]) {
							gModalGroupTooltips[i] = $gTooltip
								.clone(true)
								.html("")
								.attr({"style": ""})
								.hide()
								.appendTo($gLayer);
						}

						// ...except for the current one...
						if (this.id === currentHoveredArtifact.id) {
							gModalGroupTooltips[i].hide();
						} else {
							// Place the cloned tooltip at the center of its parent artifact
							var xPos = this.coords[0] + this.bugOffset[0];
							var yPos = this.coords[1] + this.bugOffset[1];
	
							gModalGroupTooltips[i]
								.css({left: xPos + "px", top: yPos + "px"})
								.removeClass()
								.addClass("tooltip triangle grouped satellite " + iconClass)
								.fadeIn("fast");
						}
					});
				}
			} else {
				hideTooltip(true);
			}
	    } else {
			// !currentHoveredArtifact
			hideTooltip();
			$("body").removeClass("clickable");
			gCurrentHoveredArtifact = null;
	    }
	}

	/// Take appropriate action if cursor is over a hot area
	function onStageClick(e) {
		dismissModal(e);
		closeDrawers();
	    if (gCurrentHoveredArtifact) {
	        var $artifact = $("#" + gCurrentHoveredArtifact.id, $gLayer);

			// WebTrends tag
			dcsMultiTrack('DCS.dcsuri', '/office/onenote/' + $artifact.data("props").id, 'WT.dl', '7');

			// Hash in location for deep linking
			var hash = hashify(gCurrentHoveredArtifact.tooltip);
			location = "#" + hash;

	        gCurrentHoveredArtifact = null;

	        loadModal($artifact.data("props"));
	    }
	}

	/// Center a div within the stage viewport
	function positionOnStage($div) {
	    $div
			.css({ 
				left: parseInt((($gStage.width() - $div.width()) / 2), 10) + "px",
				top: parseInt((($gStage.height() - $div.height()) / 2), 10) + "px"
			});
	}

	/// All assets for modal must be loaded before it can be displayed
	/// Order of events upon a click for an unloaded modal:
	///		* mousemovement is suspended
	///		* Screen fades in
	///		* Spinner appears
	///		* Assets complete loading
	///		* Spinner disappears
	///	A click on the screen will interrupt the process, but assets will continue to load in the background
	function loadModal(props) {

	    // Reset various settings and suspend mousemovement
	    $gTooltip.hide();
	    $gMouemoveContainer.unbind("mousemove").unbind("click");
	    $("body").removeClass("clickable");
		disableStageClicks(true);

	    // Dim the stage
		$gScreen.fadeIn(1000);
		
		// Tier [ Type, Group, Sequence ]
		//		Type is the presentation type (1, 2, 3)
		//		Group is the specific modal
		//		Sequence is which section of the modal was requested
		var tier = props.tier.split("-");

		// Has this particular modal been loaded yet?
		if (gModalLoaded[tier[0] + "-" + tier[1]]) {
			launchModal(tier, props);
		} else {
			var waitForImages = false;

			// Load all the assets (assets are zero-based while tier numbering is one-based)
			if (gModalAssets[tier[0] - 1][tier[1] - 1]) {
				$.each(gModalAssets[tier[0] - 1][tier[1] - 1], function(i) {
					if (this.type === "image") {
						waitForImages = true;
						
						var linkedItemClass = "";
						var linkedItem = false;
						if (this.linkedItem) {
							linkedItemClass = " linked";
							linkedItem = this.linkedItem;
						}
						
						$("<img/>")
							.addClass("preloader img-in-tier" + tier[0] + "-" + tier[1] + " " + this.id + linkedItemClass)
							.attr("src", this.asset)
							.data("linkedItem", linkedItem)	// Content has not been loaded yet, but when it is this image will be flagged for it
							.appendTo("body");
					}
					// No other asset types supported at this time
				});
			}

			if (waitForImages) {
				// Show the spinner
				$gLoading.stop().fadeIn();

				// Wait for all images to load
				var totalImages = gModalAssets[tier[0] - 1][tier[1] - 1].length;
				var loadedImages = [];
				$gLoadingPercent.text("0%");
				$(".img-in-tier" + tier[0] + "-" + tier[1])
					.bind("load", function() {
						loadedImages.push(true);
						$gLoadingPercent.text(parseInt(((loadedImages.length / totalImages) * 100), 10) + "%");
						if (loadedImages.length === totalImages) {
							// All assets are loaded
							gImagesLoaded = true;
							$gDownlevel.fadeOut();
							gModalLoaded[tier[0] + "-" + tier[1]] = true;
							$gLoading.fadeOut("fast", function() {
								launchModal(tier, props);
							});
						}
					});
			} else {
				gModalLoaded[tier[0] + "-" + tier[1]] = true;
				$gLoading.fadeOut("fast", function() {
					launchModal(tier, props);
				});
			}
		}

		function launchModal(tier, props) {
			var carouselLocked;
			
			$gScreen.bind("click", dismissModal);
			screenBirdsEyeView();
					
			gCurrentModal = tier[0];

			// Reset the modal to prep for its current content
			// Tiers 1 & 3 are single container; Tier 2 has two child containers
			var $t2top, $t2bottom;
			if (gCurrentModal == 2) {
				$t2top = $("<div/>").addClass("top-container");
				$t2bottom = $("<div/>").addClass("bottom-container");
				gModal[gCurrentModal].empty().append($t2top, $t2bottom).data({tier: tier, currentPage: 0});
			} else {
				gModal[gCurrentModal].empty().data({tier: tier, currentPage: 0});
			}

			// Add all preloaded images to the modal
			$(".img-in-tier" + tier[0] + "-" + tier[1]).each(function() {
				$(this)
					.clone(true)
					.removeClass("preloader")
					.addClass("item-img")
					.appendTo(gModal[gCurrentModal]);
			});

			// Add all content to the modal
			$(".tier_" + tier[0] + "-" + tier[1], "#artifacts_container")
				.clone(false)
				.removeClass("artifact")
				.appendTo(gModal[gCurrentModal])
				.find(".thumbnail")
					.remove()
				.end();

			// Move Tier 1 link text into a span
			$(".link", gModal[gCurrentModal]).each(function() {
				var $a = $("a", this);
				var $span = $("<span/>").html($a.html());
				$a.empty().append($span);
			});

			// Find each of the other artifacts in this group to see if one or more have videos
			//TODO should this be combined with the other tier[0] + "-" + tier[1] loops in this method?
			if (gModalGroup[tier[0] + "-" + tier[1]]) {
				$.each(gModalGroup[tier[0] + "-" + tier[1]], function(i) {
					prepareVideo(this, i + 1);
				});
			} else {
				prepareVideo(props, 1);
			}
			function prepareVideo(props, item) {
				if (props.mp4 && props.ogg) {
					var selector = ".tier_" + tier[0] + "-" + tier[1] + ".item_" + item;
					if (gSettings["videoT" + tier[0]].autoplay) {
						$(selector + " .link a", gModal[gCurrentModal]).bind("click", {props: props}, onVideoLinkClick);
					} else {
						showVideo(props, false);

						// Video is shown by default; no need for the links
						$(selector + " .link", gModal[gCurrentModal]).remove();
					}
				}
			}
			function onVideoLinkClick(e) {
				e.preventDefault();
				showVideo(e.data.props, true);
			}
			function showVideo(props, autoplay) {
				var $video = $("<video><source/><source/></video>")	// Bug in IE9 prevents appending <source> elements to <video> via jQuery
					.find("source:first")
						.attr({"type": gSettings.mp4Type, "src": props.mp4})
					.end()
					.find("source:last")
						.attr({"type": gSettings.oggType, "src": props.ogg})
					.end()
					.attr({
						"id": "video_player", 
                        "poster": "img/" + props.poster,
						"durationHint": 33, 
						"controls": "controls",
						"width": gSettings["videoT" + tier[0]].width, 
						"height": gSettings["videoT" + tier[0]].height
					})
					.bind("click", function(e) {
						this.blur();	// Removes the outline that IE adds when the video has focus
					});

				$("<div/>")
					.attr({ "id": "video" })
					.append($video)
					.addClass("videot" + tier[0])
					.prependTo(gModal[gCurrentModal]);

				if (autoplay) {
					$video.get(0).play();
				} else {
					$("<div/>")
						.addClass("play-button")
						.bind("click", {video: $video, props: props}, onPlayButtonClick)
						.appendTo(gModal[gCurrentModal]);
				}
			}
			function onPlayButtonClick(e) {
				dcsMultiTrack("DCS.dcsuri", "/office/onenote/" + e.data.props.id + "/video_click", "WT.dl", "7");
				e.data.video.get(0).play();
				$(this).hide();
			}
					
			// Currently tier 2 is the only modal which supports paging
			if (gCurrentModal == 2) {
				// Create the carousel container for secondary items
				$gCarousel = $("<div/>").addClass("carousel").appendTo($t2bottom);

				// Examine all linked images
				var numCarouselItems = 0;
				$(".linked", gModal[gCurrentModal]).each(function() {
					var $img = $(this);
					var $div = $(".item_" + $img.data("linkedItem"), gModal[gCurrentModal]);

					if ($img.data("linkedItem") == tier[2]) {
						// Clicked item gets hero treatment
						$img.addClass("hero");
						$div.addClass("hero");
					} else {
						// Move non-hero linked items into the carousel
						$gCarousel.append($img, $div);
						numCarouselItems++;
					}
				});
				gModal[gCurrentModal].data("pages", Math.ceil(numCarouselItems / gCarouselItemsPerPage));
						
				// Measure container width (must make modal "visible" first)
				gModal[gCurrentModal].css("left", "-10000px").show();
				$gCarousel.data("containerWidth", $t2bottom.width());
			}

			// Move Tier 3 images into a container
			if (gCurrentModal == 3) {
 				$("img, div.play-button", gModal[gCurrentModal]).each(function() {
					$gThumbnail = $("<div/>").addClass("thumbnail").prependTo(gModal[gCurrentModal]);
					var $this = $(this);
					$gThumbnail.append($this);
				});
			}

			// Add the navigation buttons
			//TODO refactor so that click handler is bound only once (in document.ready)
			$gCloseModal.bind("click", dismissModal).appendTo(gModal[tier[0]]);
					
			var prevButton = $("<div/>")
				.addClass("page-prev")
				.html("&#9668;")
				.bind("click", pagePrev);
			var nextButton = $("<div/>")
				.addClass("page-next")
				.html("&#9658;")
				.bind("click", pageNext);
			$gModalPageNav.empty().appendTo(gModal[tier[0]]).append(prevButton, nextButton);
			pageBy(0);
			showHidePaging();
					
			// Offset of the modal
			var leftOffset = parseInt(((gStageWidth - gModal[tier[0]].width()) / 2), 10);
			var topOffset = parseInt(((gStageHeight - gModal[tier[0]].height()) / 2), 10);
				
			gModal[gCurrentModal]
				.hide()
				.css({left: leftOffset + "px", top: topOffset + "px"})
				.removeClass()
				.addClass("modal tier" + tier[0] + "-" + tier[1])
				.delay(400)
				.fadeIn(600);

			function pagePrev() { pageBy(-1); }
			function pageNext() { pageBy(1); }
			function pageBy(increment) {
				if (gCurrentModal) {
					var modal = gModal[gCurrentModal];
					var tier = modal.data("tier");
					if (modal.data("pages")) {
						if (tier[0] !== 1) {
							if (!carouselLocked) {
								carouselLocked = true;
								var moveTo;

								if (increment === 0) {
									moveTo = "+=0";
								} else if (increment > 0) {
									moveTo = "-=" + $gCarousel.data("containerWidth");
								} else {
									moveTo = "+=" + $gCarousel.data("containerWidth");
								}

								$gCarousel
									.animate({ left: moveTo }, 500, "easeInOutQuad", function () {
										modal.data("currentPage", modal.data("currentPage") + increment);
										showHidePaging();
										carouselLocked = false;
									});
							}
						}
					}
				}
			}
			function showHidePaging() {
				if (gCurrentModal) {
					var modal = gModal[gCurrentModal];
					if (modal.data("pages")) {
						var numberOfPages = modal.data("pages");
						var currentPage = modal.data("currentPage");

						if (currentPage === 0) { $(".page-prev", modal).hide(); }
						else { $(".page-prev", modal).show(); }

						if (currentPage === numberOfPages - 1) { $(".page-next", modal).hide(); }
						else { $(".page-next", modal).show(); }
					}
				}
			}
		}

	}

	/// By definition, only one modal at a time can be visible
	function dismissModal(e) {
		if (gCurrentModal) {
			if (e) { e.stopPropagation(); }
		
			var modal = gModal[gCurrentModal];
	        modal.fadeOut(100).data({tier: null, currentPage: null}).empty();
			gCurrentModal = null;

			$gScreen.hide().unbind("click");

			$gMouemoveContainer
				.bind("mousemove", onStageMouseMove)
				.bind("click", onStageClick);

			enableStageClicks(true);
			onStageMouseMove(e);

			// Remove hash, if it exists
			location.hash = "";
	    }
	}

	/// Update the size and position of the Birds Eye View mask depending on the position of the stage
	function updateBirdsEyeMask(layerPos) {
		
		// Ratio of actual layer size to BirdsEye viewer size
		var widthRatio = gBirdsEyeWidth / gLayerWidth;
		var heightRatio = gBirdsEyeHeight / gLayerHeight;
		
		var curLayerPos = (layerPos || getLayerPos());

		// negative coordinates of the layer are used as positive offsets for the mask
		curLayerPos[0] = -curLayerPos[0];
		curLayerPos[1] = -curLayerPos[1];
		
		// Corners of the mask
		var tl = [], tr = [], bl = [], br = [];

		tl[0] = curLayerPos[0] * widthRatio;
		tl[1] = curLayerPos[1] * heightRatio;

		tr[0] = (gStageWidth + curLayerPos[0]) * widthRatio;
		tr[1] = curLayerPos[1] * heightRatio;

		bl[0] = curLayerPos[0] * widthRatio;
		bl[1] = (gStageHeight + curLayerPos[1]) * heightRatio;

		br[0] = (gStageWidth + curLayerPos[0]) * widthRatio;
		br[1] = (gStageHeight + curLayerPos[1]) * heightRatio;

		var canvas = $gBirdsEyeMask[0];
		var context = canvas.getContext("2d");

		// Clear any existing mask
		context.clearRect(0, 0, canvas.width, canvas.height);
		var w = canvas.width;
		canvas.width = 1;
		canvas.width = w;

		context.fillStyle = "rgba(0, 0, 0, 0.5)";

		context.beginPath();

			// Draw background box clockwise
			context.moveTo(0, 0);
			context.lineTo(gBirdsEyeWidth, 0);
			context.lineTo(gBirdsEyeWidth, gBirdsEyeHeight);
			context.lineTo(0, gBirdsEyeHeight);
			context.lineTo(0, 0);
                    
			// Draw unmasked area counter-clockwise
			context.moveTo(tl[0], tl[1]);
			context.lineTo(bl[0], bl[1]);
			context.lineTo(br[0], br[1]);
			context.lineTo(tr[0], tr[1]);
			context.lineTo(tl[0], tl[1]);
          
		context.closePath();
		context.fill();
	}
	function toggleBirdsEyeView(e) {
		e.stopPropagation();
		if ($gBirdsEyeContainer.data("down")) {
			$gBirdsEyeContainer
				.stop()
				.animate({"top": "-154px"}, 300, "easeInOutQuad", function() {
					$gBirdsEyeDock.removeClass("active");
				})
				.data("down", false);
		} else {
			$gBirdsEyeContainer
				.stop()
				.animate({"top": "30px"}, 300, "easeInOutQuad")
				.data("down", true);
			$gBirdsEyeDock.addClass("active");
		}
	}
	function screenBirdsEyeView() {
		var canvas = $gBirdsEyeMask[0];
		var context = canvas.getContext("2d");

		// Clear any existing mask
		context.clearRect(0, 0, canvas.width, canvas.height);
		var w = canvas.width;
		canvas.width = 1;
		canvas.width = w;

		context.fillStyle = "rgba(0, 0, 0, 0.5)";

		context.beginPath();

			context.moveTo(0, 0);
			context.lineTo(gBirdsEyeWidth, 0);
			context.lineTo(gBirdsEyeWidth, gBirdsEyeHeight);
			context.lineTo(0, gBirdsEyeHeight);
			context.lineTo(0, 0);
          
		context.closePath();
		context.fill();
	}

	/// Toggle the drawer open or closed
	/// Close the other drawer if it is currently open
	function toggleEssentials() {
		toggleDrawer($gEssentialsContainer, "essentials");
	}
	function toggleHowTo() {
		toggleDrawer($gHowToContainer, "howto");
	}
	function toggleAnswers() {
		toggleDrawer($gAnswersContainer, "answers");
	}
	function toggleDrawer($drawer, buttonClass) {
		var drawerRaised = false;
		
		// raise any other drawers that are currently down
		$(".list-drawer").each(function() {
			var $other = $(this);
			
			if ($other.data("down") && $other.attr("id") != $drawer.attr("id")) {
				listDrawerUp($other, function() { doToggle($drawer); });
				drawerRaised = true;
			}
		});

		if (!drawerRaised) { doToggle($drawer); }
	}
	function doToggle($drawer) {
		if ($drawer.data("down")) {
			listDrawerUp($drawer);
		} else {
			listDrawerDown($drawer);
		}
	}
	function closeDrawers() {
		$(".list-drawer", $gStage).each(function() {
			if ($(this).data("down")) { listDrawerUp($(this)); }
		});
	}
	function listDrawerUp($drawer, callback) {
		var which = $drawer.attr("id").substr($drawer.attr("id").indexOf("_") + 1);
		$drawer
			.stop()
			.animate({"top": "-154px"}, 300, "easeInOutQuad", callback)
			.data("down", false);
		$("div." + which, $gDrawerDock).removeClass("active");
	}
	function listDrawerDown($drawer) {
		var which = $drawer.attr("id").substr($drawer.attr("id").indexOf("_") + 1);
		$drawer
			.stop()
			.animate({"top": "30px"}, 300, "easeInOutQuad", function() {
				$("div." + which, $gDrawerDock).addClass("active");
			})
			.data("down", true);
	}

	function disableClick(e) {
		e.preventDefault();
	}

	function testBrowserPerformance(callback) {
		var start = new Date();

		var count = 0;
		var p = setInterval(loop, 30);
		function loop() {
			var i, temp, end;
			for (i = 0; i < 1000; i++) {
				temp = {};
			}

			if (count++ >= 3) {
				clearInterval(p);
				end = new Date();
				performanceIndex = (end - start);

				callback.call(null, (performanceIndex < gPerformanceLitmus));
			}
		}

	}

	function debug(str) {
	    try {
	        if (console) { console.log(str); }
	    } catch (e) { }
	}

	/// Determine the current offset of the layer
	function getLayerPos() {
		
		var layerPosX, layerPosY;

		// Layer position is always negative, although it can approach zero
		layerPosX = parseInt($gLayer.css("left"), 10);
		layerPosY = parseInt($gLayer.css("top"), 10);

		return [layerPosX, layerPosY];
	}

	/// Point is inside a polygon if the number of edges on each side 
	///		of the point (on a horizontal plane) is an odd number.
	///	Parameters:
	///		coords: [x, y] coordinates of the mouse pointer
	///		polygon: an array of [x,y] coordinates of the polygon
	function isInShape(coords, polygon) {
	    var x = coords[0], y = coords[1];
	    var polyX = [], polyY = [];
	    var corners = (polygon.length / 2);
	    var i, j = corners - 1;
	    var oddNodes = false;

	    $.each(polygon, function (i) {
	        if (i % 2 === 0) { polyX.push(this); }
	        else { polyY.push(this); }
	    });

	    for (i = 0; i < corners; i++) {
			if ((polyY[i] < y && polyY[j] >= y) || (polyY[j] < y && polyY[i] >= y)) {
				if (polyX[i] + (y - polyY[i]) / (polyY[j] - polyY[i]) * (polyX[j] - polyX[i]) < x) {
					oddNodes = !oddNodes;
				}
			}
			j = i;
	    }

	    return oddNodes;
	}

	/// Strip out all non-alphanumeric characters and replace spaces with dashes
	function hashify(str) {
		var retr = str.replace(/[^a-zA-Z0-9 ]/g, "");
		retr = retr.replace(/\s/g, "-");
		return retr.toLowerCase();
	}

})(jQuery);
