/**
 * @projectDescription Home page related JavaScript code.
 * @author edwin edwin@8projects.nl
 * @version 1.0
 * 
 * @classDescription Namespace for common utility functions.
 * 
 * Copyright (C) 2008 8Projects -- Released under GNU Lesser General Public License.
 * For terms and conditions, see the gpl.txt and lgpl.txt files on http://www.8projects.nl/doc/.
 */
var Util = {
	_onLoad: function() {
		this.startMouseMoveListener();	
	},
	
	_onUnLoad: function() {
		this.stopMouseMoveListener();
	},
	
	/**
	 * Sniffer to see if browser is IE6 or less.
	 */
	isIE6: function() {
		if (Prototype.Browser.IE) {
			if (!this._versionIE) {
				var msie = navigator.appVersion.indexOf("MSIE");
				if (msie != -1) {
					this._versionIE = parseInt(navigator.appVersion.substring(msie + 5));
				}
			}
			return this._versionIE < 7;
		}
		return false;
	},
		
	/**
	 * Retrieve a query parameter from the document's URL.
	 * 
	 * @param {String} name Name of the query parameter.
	 * @param {String} defaultValue Default value.
	 * @param {String} url If specified, the given URL is used to retrieve the query parameter from
	 * 					   instead of the current document's URL.
	 * @return The parameter value, or the default value if it wasn't specified.
	 */
	getUrlParameter: function(name, defaultValue, url) {
		var params = null;
		if (url == null) {
			params = window.location.search;
			if (params.charAt(0) == "?") {
				params = params.substring(1);
			}
		} else {
			var qIndex = url.indexOf("?");
			if (qIndex != -1) {
				params = url.substring(qIndex + 1);
			}
		}
		if (params != null && params.length > 0) {
			var split = params.split("&");
			for( var i = 0; i < split.length; ++i ) {
				var param = split[i].split("=");
				if (param[0] == name) {
					var value = param[1].strip();
					if (value.length > 0) {
						return value;
					}
				}
			}
		}
		return defaultValue;
	},
	
	/**
	 * Utility that changes all links on the page with class 'external' to open
	 * their destinations in a new window. Should be called in the 'load' event
	 * of the page.
	 */
	fixExternalLinks: function() {
		$$("a.external").each(function(link) {
			link.target = "_blank";
		});
	},
	
	/**
	 * A global mouse motion listener. Mainly used by the Util.Popup class to have access
	 * to the current mouse pointer position.
	 * Having a single listener is more efficient that adding one for each popup that
	 * is created. This method in combination with Util.stopMouseMoveListener() maintain a
	 * usage count, and only if the count reaches zero, is the mouse listener removed
	 * from the window.
	 */
	startMouseMoveListener: function() {
		this._mouseMoveListenerCount = (this._mouseMoveListenerCount || 0) + 1;
		if (this._mouseMoveListenerCount == 1) {
			this._mouseMoveListener = this._onMouseMove.bindAsEventListener(this);
			Event.observe(document, "mousemove", this._mouseMoveListener);
		}
	},
	
	/**
	 * Stop the global mouse motion listener. The listener is only stopped if this
	 * is the upper-most call, corresponding with the first call to Util.startMouseListener(). 
	 */
	stopMouseMoveListener: function() {
		this._mouseMoveListenerCount = (this._mouseMoveListenerCount || 0) - 1;
		if (this._mouseMoveListenerCount == 0) {
			Event.stopObserving(document, "mousemove", this._mouseMoveListener);
			this._mouseMoveListener = null;
		}		
	},
	
	_onMouseMove: function(e) {
		var pos = Event.pointer(e);
		this._pointerX = pos.x;
		this._pointerY = pos.y;
	},
	
	/**
	 * Move an element by the specfied delta's in pixels.
	 * 
	 * @param {Object} e Element or ID of element to move.
	 * @param {Object} dx Delta x in pixels.
	 * @param {Object} dy Delta y in pixels.
	 */
	moveElement: function(e, dx, dy) {
		e.style.left = (parseInt(Element.getStyle(e, "left")) + dx) + "px";
		e.style.top = (parseInt(Element.getStyle(e, "top")) + dy) + "px";
	},
	
	/**
	 * Resize an element by the specfied delta's in pixels.
	 * 
	 * @param {Object} e Element or ID of element to resize.
	 * @param {Object} dx Delta x in pixels.
	 * @param {Object} dy Delta y in pixels.
	 */
	resizeElement: function(e, dx, dy) {
		e.style.width = (parseInt(Element.getStyle(e, "width")) + dx) + "px";
		e.style.height = (parseInt(Element.getStyle(e, "height")) + dy) + "px";
	},
	
	/**
	 * General center function. Centers the given rectangle within the visible browser area.
	 * Returns an array with two elements, the future left and top coordinates of the rectangle.
	 * 
	 * @param {Number} width Width of the rectangle to center.
	 * @param {Number} height Height of the rectangle to center.
	 * @param {Boolean} dontUseScrollOffsets
	 * 		If false or not specified, the centering takes into
	 * 		account the current scrollbar positions when centering so that the returned coordinates
	 * 		are in the visible part of the document. If true, the centering is just done with
	 * 		[0, 0] as the left/top coordinates of the viewport.
	 */
	centerOverVisible: function(width, height, dontUseScrollOffsets) {
		var size = Util.getWindowSize();
		var offsets = dontUseScrollOffsets ? [0,0] : Util.getScrollOffsets();
		var left = (size[0] - Math.floor(width - 10)) / 2 + offsets[0];
		var top = Math.max(20, Math.floor((size[1] - height) / 2)) - offsets[1];
		return [left, top];
	},
	
	/**
	 * Center function to center the given rectangle over the browser (useful for popups).
	 * Returns an array with two elements, the future left and top coordinates of the rectangle.
	 * 
	 * @param {Object} width
	 * @param {Object} height
	 */
	centerOverWindow: function(width, height) {
		var location = Util.getScreenLocation();
		var size = Util.getWindowSize();
		var left = location[0] + Math.round((size[0] - width) / 2);
		var top = location[1] + Math.round((size[1] - height) / 2);
		return [left, top];
	},
	
	/**
	 * Center function to center the given rectangle on the user's screen (useful for popups).
	 * Returns an array with two elements, the future left and top coordinates of the rectangle.
	 * 
	 * @param {Object} width
	 * @param {Object} height
	 */
	centerOnScreen: function(width, height) {
		var screen = Util.getScreenSize();
		var left = Math.round((screen[0] - width) / 2);
		var top = Math.round((screen[1] - height) / 2);
		return [left, top];
	},
	
	/**
	 * Get the location of the browser window on the user's screen.
	 * Note that for IE this returns the coords of the client area, not of the browser window.
	 */
	getScreenLocation: function() {
		if (window.screenLeft != null) {
			return [window.screenLeft, window.screenTop];
		} else {
			return [window.screenX, window.screenY];
		}
	},
	
	/**
	 * Get the size of the user's screen.
	 * Returns an array of two elements, the width and height of the screen.
	 */
	getScreenSize: function() {
		return [window.screen.availWidth, window.screen.availHeight];
	},
	
	/**
	 * Get the size of the current document body.
	 * Returns an array of two elements, the width and height of the complete document.
	 */
	getBodySize: function() {
		var width = 0, height = 0;
		width = document.documentElement.scrollWidth;
		height = document.documentElement.scrollHeight;
		return [width, height];
	},

	/**
	 * Get the size of the current browser window.
	 * Returns an array of two elements, the width and height of the window in pixels.
	 */
	getWindowSize: function() {
		var width = 0, height = 0;
		if( typeof window.innerWidth == 'number') {
			// Non-IE
			width = window.innerWidth;
			height = window.innerHeight;
	
		} else if (document.documentElement && (document.documentElement.clientWidth || document.documentElement.clientHeight)) {
			// IE 6+ in 'standards compliant mode'
			width = document.documentElement.clientWidth;
			height = document.documentElement.clientHeight;
	
		} else if (document.body && (document.body.clientWidth || document.body.clientHeight)) {
			// IE 4 compatible
			width = document.body.clientWidth;
			height = document.body.clientHeight;
		}
		return [width, height];
	},
	
	/**
	 * Retrieve the scroll offsets of the window.
	 * Returns an array of two elements, where the first element contains the amount the
	 * window was scrolled horizontally, and the second the amount scrolled vertically.
	 */
	getScrollOffsets: function() {
		var offsets = [0, 0];
		
		if (window.pageXOffset) {
			offsets[0] = window.pageXOffset
		} else if (document.documentElement && document.documentElement.scrollLeft) {
			offsets[0] = document.documentElement.scrollLeft;
		} else if (document.body) {
			offsets[0] = document.body.scrollLeft;
		}
		
		if (window.pageYOffset) {
			offsets[1] = window.pageYOffset;
		} else if (document.documentElement && document.documentElement.scrollTop) {
			offsets[1] = document.documentElement.scrollTop;
		} else if (document.body) {
			offsets[1] = document.body.scrollTop;
		}
		
		return offsets;
	},
	
	/**
	 * Block the entire browser window from receiving input.
	 * Calls to this method can be nested; only the outer-most call to Util.unblockWindow()
	 * removes the blocker.
	 * 
	 * Uses the CSS class "window_blocker" to style the blocking DIV.
	 */
	blockWindow: function(options) {
		// Only block on top-level call.
		this._blockCount = (this._blockCount || 0) + 1;
		if (this._blockCount == 1) {
			var blocker = this._blocker;
			if (blocker == null) {
				blocker = document.createElement("DIV");
				blocker.className = "window_blocker";
				blocker.style.display = "none";
				document.body.appendChild(blocker);
				this._blocker = blocker;
			}
			this._blockerOptions = {
				fading: true,
				fadeDuration: 1.0				
			};
			if (options != null) {
				Object.extend(this._blockerOptions, options);
			}
			
			// Width/height 100% does not work in IE.
			if (Util.isIE6()) {
				// This must be IE6 or less since IE7 supports "fixed" (set in site.css: div.window_blocker).
				var size = Util.getBodySize();
				var windowSize = Util.getWindowSize();
				blocker.style.width = Math.max(size[0], windowSize[0]) + "px";
				blocker.style.height =  Math.max(size[1], windowSize[1]) + "px";
				
				// If window is resized, updated blocker size/position.
				this._windowResizeTimer = window.setInterval(this._windowBlockerAdjuster, 100);	
				Event.observe(window, "resize", this._windowBlockerResizer);				
			}
			
			if (this._blockerOptions.fading) {
				Effect.Appear(blocker, { duration: this._blockerOptions.fadeDuration, to: Element.getStyle(blocker, "opacity") });
			} else {
				Element.show(blocker);
			}
		}
	},
	
	/**
	 * Undo the blocking done by Util.blockWindow().
	 */
	unblockWindow: function() {
		// Only unblock on top-level call.
		this._blockCount = (this._blockCount || 0) - 1;
		if (this._blockCount == 0) {
			var blocker = this._blocker;
			if (this._blocker) {
				if (Util.isIE6()) {
					// Stop scroll/resize event monitoring.
					window.clearInterval(this._windowResizeTimer); 
					this._windowResizeTimer = null;
					Event.stopObserving(window, "resize", this._windowBlockerResizer);
				}

				if (this._blockerOptions.fading) {
					Effect.Fade(blocker, { duration: this._blockerOptions.fadeDuration });
				} else {
					Element.hide(blocker);
				}
			}
		}
	},
	
	_windowBlockerResizer: function() {
		Util._windowDimensionChanged = true;
	},

	_windowBlockerAdjuster: function() {
		if (Util._windowDimensionChanged) {
			Util._windowDimensionChanged = false;
			
			var blocker = Util._blocker;
			if (blocker != null) {
				var size = Util.getBodySize();
				var windowSize = Util.getWindowSize();
				blocker.style.width = Math.max(size[0], windowSize[0]) + "px";
				blocker.style.height =  Math.max(size[1], windowSize[1]) + "px";
			}
		}
	},
	
	/**
	 * Add a hash change listener. From now on, if the user presses the back
	 * or forward button, all hash change listeners are notified with the new value of
	 * the hash. Programmatic changes to the hash should always be done using
	 * the Util.changeHash() function.
	 * 
	 * This is very simple history (back button) stuff. Only works on FF and Safari, not on IE and Opera.
	 * Should be a wrapper around something like dhtmlHistory.js to make it work everywhere (almost).
	 * 
	 * @param {Object} listener Method to be called with the new hash value.
	 */
	addHashChangeListener: function(listener) {
		if (this._hashChangeListeners.length == 0) {
			this._initHashChange();
		}
		if (this._isSupported) {
			this._hashChangeListeners.push(listener);
		}
	},
	
	/**
	 * Remove a previously registered hash change listener.
	 * 
	 * @param {Object} listener
	 */
	removeHashChangeListener: function(listener) {
		if (this._isSupported) {
			for (var i = 0; i < this._hashChangeListeners.length; ++i) {
				if (this._hashChangeListeners[i] == listener) {
					delete this._hashChangeListeners[i];
					break;
				}
			}
			if (this._hashChangeListeners.length == 0 && this._hashCheckTimer != null) {
				this._cleanupHashChange();
			}
		}
	},
	
	/**
	 * Use this to programmatically change the hash value, to store an application state.
	 * If the hash property of window.location is changed manually, it will trigger a
	 * superfluous hash change event.
	 * 
	 * @param {Object} newValue New value for the hash part of the current URL.
	 */
	changeHash: function(newValue) {
		if (this._isSupported) {
			this._ignoreNextHashChange = true;
			window.location.hash = newValue;
		}
	},
	
	_getHashValue: function() {
		var hash = window.location.hash;
		if (hash != null && hash.length >= 1 && hash.charAt(0) == "#") {
			hash = hash.substring(1);
		}
		if (hash.length == 0) {
			hash = null;
		}
		return hash;
	},
	
	_initHashChange: function() {
		this._isSupported = !Prototype.Browser.IE && !Prototype.Browser.Opera;
		if (this._isSupported) {
			this._hashCheckTimer = window.setInterval(this._checkHash.bind(this), this._hashCheckInterval);
			this._lastHashValue = this._getHashValue();
		}
	},
	
	_cleanupHashChange: function() {
		if (this._isSupported) {
			window.clearInterval(this._hashCheckTimer);
			this._hashCheckTimer = null;
		}
	},
	
	_checkHash: function() {
		if (this._ignoreNextHashChange) {
			this._ignoreNextHashChange = false;
			this._lastHashValue = this._getHashValue();
			
		} else {
			var newValue = this._getHashValue();
			if (newValue != this._lastHashValue) {
				this._lastHashValue = newValue;

				// Hash has changed, notify listeners.
				for( var i = 0; i < this._hashChangeListeners.length; ++i ) {
					var listener = this._hashChangeListeners[i];
					if (listener != null) {
						listener(newValue);
					}
				}
			}
		}
	},
	
	_hashChangeListeners: [],
	_hashCheckInterval: 100,
	_ignoreNextHashChange: false,
	
	_monthNames: {
		"en": ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],		
		"nl": ["Januari", "Februari", "Maart", "April", "Mei", "Juni", "Juli", "Augustus", "September", "Oktober", "November", "December"]		
	},
	
	/**
	 * Retrieve name of the given month, January has number 0.
	 * The names returned always start with an upper-case character.
	 * 
	 * @param {int} month Month number.
	 * @param {String} langCode Only 'en' and 'nl' supported. Defaults to 'en'.
	 * 
	 * @return A month or null if an invalid month number or language code was given.
	 */
	getMonthName: function(month, langCode) {
		var names = this._monthNames[langCode || "en"];
		if (!names) {
			return null;
		}
		return names[month];
	},

	/**
	 * Pad a number with zeros (to the left) to a specified width.
	 * @param {Object} n Number to pad.
	 * @param {Object} width Width of result string.
	 * @return The padded string.
	 */
	padNumber: function(n, width) {
		var s = new String(n);
		var count = width - s.length;
		if (count <= 0) {
			return s;
		}
	
		for( var i = 0; i < count; ++i ) {
			s = "0" + s;
		}
	
		return s;
	}
};

/**
 * Constructor.
 * @constructor
 * 
 * @classDescription Class that can provide messages in a specific language, and that can update
 * elements of the DOM tree when the language code is changed.
 * 
 * @param {Object} langCode Language code.
 */
Util.MessageSource = function(langCode) {
	this.langCode = langCode;
	this.idPrefixes = ["", "hdr_", "lnk_", "btn_", "lbl_", "txt_", "tab_"];
	this.keyModifiers = ["title", "alt", "value"];
};

/**
 * Message maps. Should be defined separately.
 * Contains the language code as key, and a map with key:message entries.
 */
Util.MessageSource.messages = {};

Util.MessageSource.prototype = {
	/**
	 * Retrieve a message, returning the default message if the given key is not found.
	 * If no default message is specified, the capitalized key is returned.
	 * 
	 * @param {Object} key Message key.
	 * @param {Object} defaultMessage Optional default message.
	 */
	get: function(key, defaultMessage) {
		var msg = null;
		
		var messages = Util.MessageSource.messages[this.langCode];
		if (messages != null) {
			msg = messages[key];
		}

		if (msg == null) {
			if (defaultMessage != null) {
				msg = defaultMessage;
			} else {
				msg = key.capitalize();
			}
		}
		
		return msg;
	},
	
	/**
	 * Change the language code.
	 * 
	 * @param {Object} newLangCode New language code.
	 */
	change: function(newLangCode) {
		this.langCode = newLangCode;
		
		var map = Util.MessageSource.messages[this.langCode];
		if (map) {
			Object.keys(map).each(function(key) {
				var splitKey = this.splitKey(key);
				var baseKey = splitKey[0];
				var modifier = splitKey[1];
				
				this.idPrefixes.each(function(prefix) {
					var e = $(prefix + baseKey);
					if (e) {
						var msg = this.get(key);
						if (modifier == null) {
							e.innerHTML = msg;
						} else {
							e[modifier] = msg;
						}
					}
				}, this);
			}, this);
		}
	},
	
	/**
	 * Split a message key into the actual key and its modifier (key:modifier).
	 * 
	 * @param {Object} key The full key to split.
	 * @return An array: at index 0, the key value, at index 1 the modifier or null.
	 */
	splitKey: function(key) {
		for( var i = 0; i < this.keyModifiers.length; ++i ) {
			var m = this.keyModifiers[i];
			if (key.endsWith(":" + m)) {
				return [key.substring(0, key.length - m.length - 1), m];
			}
		}
		return [key, null];
	}
};

/**
 * Class that handles a tab strip, and showing the div belonging a clicked tab.
 * The tab strip, identified by listId, is assumed to be an UL or other list container,
 * that contains LI elements, which each contain an A element. An event handler is
 * attached to each A element so that if the n-th link in the list is clicked, the
 * n-th tab DIV in the divIds list is made visible. Only one div is ever visible.
 * 
 * @param {Object} listId ID of list containing tab links (normally an UL).
 * @param {Object} tabIds List of div ID's or divs.
 * @param {Object} defaultTab Default tab div ID.
 * 
 * @constructor
 */
Util.TabManager = function(listId, divIds, defaultTab){
	// Process tab links.
	this.list = $(listId);
	this.links = [];
	var lis = this.list.getElementsByTagName("LI");
	for( var i = 0; i < lis.length; ++i ) {
		this.enabled[i] = true;
		var li = lis[i];
		var selected = Element.hasClassName(li, "tab_selected");

		// Retrieve background color from LI to be used in morphing later.		
		var bgColor = Element.getStyle(li, "background-color");
		if (selected) {
			this.tabColors.selectedBg = bgColor;
		} else {
			this.tabColors.unselectedBg = bgColor;
		}
		
		var link = li.getElementsByTagName("A")[0];
		if (link != null) {
			// Retrieve foreground color from A to be used in morphing later.
			var fgColor = Element.getStyle(link, "color");
			if (selected) {
				this.tabColors.selectedFg = fgColor;
			} else {
				this.tabColors.unselectedFg = fgColor;
			}
			link.onclick = this.showTab.bind(this, i);
		}
		this.links.push(link); // Also add null-links.
	}
	
	// Collect content divs.
	this.divs = [];
	var divMap = {};
	divIds.each(function(divId) {
		var div = $(divId);
		this.divs.push(div);
		
		if (div != null) {
			divMap[divId] = this.divs.length - 1;
		}
	}, this);

	// Show specified tab.
	if (defaultTab != null) {
		this.defaultTabIndex = this.findTabIndexFromDivId(defaultTab);
		if (this.defaultTabIndex == -1) {
			this.defaultTabIndex = 0;
		}
	}
	if (this.defaultTabIndex != 0) {
		this.selectedTabIndex = this.defaultTabIndex;
	}
	
	// Find other links to tab divs, and let them select the appropriate tab.
	var links = document.getElementsByClassName("tab_link");
	for( var i = 0; i < links.length; ++i ) {
		var link = links[i];
		var tabName = Util.getUrlParameter("tab", null, link.href);
		if (tabName != null) {
			var tabIndex = divMap["div_" + tabName];
			if (tabIndex != null) {
				link.onclick = this.showTab.bind(this, tabIndex);
			}
		}
	}
	
	// If hash part of URL is set, use it to determine initial tab.
	var hash = document.location.hash;
	if (hash.length > 0) {
		var divId = hash.substring(1);
		var tabIndex = this.findTabIndexFromDivId(divId);
		if (tabIndex != -1) {
			this.showTab(tabIndex);
		}
	}
}

Util.TabManager.prototype = {
	/**
	 * Currently selected tab index. Do not modify directly.
	 */
	selectedTabIndex: 0,
	
	/**
	 * Duration of a single fade in/out of a tab div.
	 */
	fadeDuration: 0.3,
	
	/**
	 * Whether this tab manager stores the selected DIV id in the hash part of the URL.
	 * Enable only using the setStoreTabState() method.
	 */
	storeTabState: false,
	
	/**
	 * Map from tab index to boolean: if true, tab link is enabled, else disabled.
	 */
	enabled: {},
	
	/**
	 * Tab colors. Normally retrieved from the styles of the tab LI's (background colors)
	 * and A's (foreground colors), so not needed to change this manually.
	 */
	tabColors: {
		selectedFg: "#000",
		selectedBg: "#0f0",
		unselectedFg: "#0f0",
		unselectedBg: "#000"
	},
	
	/**
	 * Call this to enable storing the selected tab in the hash part of the URL,
	 * allowing for back button handling (goes to previously selected tab).
	 * 
	 * @param {Object} value True or false. Defaults to false, since it can only
	 * 						 be enabled for one Util.TabManager instance on the page.
	 */
	setStoreTabState: function(value) {
		this.storeTabState = value;
		if (value) {
			this._hashChangeListener = this.onHashChanged.bind(this);
			Util.addHashChangeListener(this._hashChangeListener);
		} else {
			if (this._hashChangeListener != null) {
				Util.removeHashChangeListener(this._hashChangeListener);
				this._hashChangeListener = null;
			}
		}
	},
	
	/**
	 * Handler for hash change, i.e. when the user has pressed the back or forward button.
	 * 
	 * @param {Object} newValue
	 */
	onHashChanged: function(newValue) {
		if (newValue == null) {
			this.showTab(this.defaultTabIndex);
		} else {
			var tabIndex = this.findTabIndexFromDivId(newValue);
			this.showTab(tabIndex);
		}
	},
	
	/**
	 * Find the tab index of the given tab div ID.
	 * 
	 * @param {Object} divId ID of div.
	 * @return Tab index belonging to the specified div, or -1 if the div was not found.
	 */
	findTabIndexFromDivId: function(divId) {
		for( var i = 0; i < this.divs.length; ++i ) {
			var div = this.divs[i];
			if (div != null && div.id == divId) {
				return i;
			}
		}
		return -1;
	},

	/**
	 * Show a tab given its index in the tab list.
	 * 
	 * @param {Object} index Tab index, starts at 0.
	 */
	showTab: function(index) {
		if (this.enabled[index] && index != this.selectedTabIndex) {
			var nextIndex = index;
			var previousIndex = this.selectedTabIndex;
			this.selectedTabIndex = nextIndex;
	
			var nextLink = this.links[nextIndex];
			var nextDiv = this.divs[nextIndex];
			
			if (this.storeTabState && nextDiv != null) {
				Util.changeHash(nextDiv.id);
			}
			
			var previousLink = this.links[previousIndex];
			var previousDiv = this.divs[previousIndex];
			
			// Check if the tab link is a link to a tab (has a "tab" query parameter),
			// if not, assume the link is a real link, and just follow it.
			var goURL = null;
			if (Util.getUrlParameter("tab", null, nextLink.href) == null) {
				goURL = nextLink.href;
			}
			
			this.doShowNextTab(nextLink, nextDiv, previousLink, previousDiv);
			
			if (goURL) {
				document.location.href = goURL;
				return true;
			}
		}
		return false;
	},
	
	/**
	 * Enable or disable a tab. If a tab is disabled, it does not react to mouse clicks.
	 * Does not have an effect on a tab that is already active.
	 * 
	 * @param {Object} index Tab index, starts at 0.
	 */
	enableTab: function(index, enable) {
		this.enabled[index] = enable ? true : false; 
	},
	
	/**
	 * Return whether the given tab is enabled or not.
	 * 
	 * @param {Object} index Tab index.
	 */
	isTabEnabled: function(index) {
		return this.enabled[index];
	},
	
	/**
	 * Actual method that makes a selected tab visible, and the previous invisible.
	 * Could be overridden to provide your own implementation of tab selection.
	 * 
	 * @param {Object} nextLink Tab link to make appear selected.
	 * @param {Object} nextDiv Tab div to make visible.
	 * @param {Object} previousLink Tab link to make appear unselected.
	 * @param {Object} previousDiv Tab div to make invisible.
	 */
	doShowNextTab: function(nextLink, nextDiv, previousLink, previousDiv) {
		// Morph tabs from previous to next.
		if (nextLink) {
			new Effect.Morph(nextLink, {
				style: { color: this.tabColors.selectedFg },
				duration: this.fadeDuration
			});
			new Effect.Morph(nextLink.parentNode, {
				style: { backgroundColor: this.tabColors.selectedBg },
				duration: this.fadeDuration
			});
		}
		
		if (previousLink) {
			new Effect.Morph(previousLink, {
				style: { color: this.tabColors.unselectedFg },
				duration: this.fadeDuration
			});
			new Effect.Morph(previousLink.parentNode, {
				style: { backgroundColor: this.tabColors.unselectedBg },
				duration: this.fadeDuration
			});
		}

		// Slightly convoluted code to get the fades/appears of the divs done in the right order.
		if (previousDiv) {
			if (nextDiv) {
				Effect.Fade($("footer"),  { duration: this.fadeDuration });
			}
			Effect.Fade(previousDiv, { duration: this.fadeDuration, afterFinish: function() {
				if (nextDiv) {
					Effect.Appear(nextDiv, { duration: this.fadeDuration, queue: "end", afterFinish: function() {
						Effect.Appear($("footer"), { duration: this.fadeDuration });
					}.bind(this)});
				}
			}.bind(this)});
		} else if (nextDiv) {
			Effect.Appear(nextDiv, { duration: this.fadeDuration, queue: "end", afterFinish: function() {
				Effect.Appear($("footer"), { duration: this.fadeDuration });
			}.bind(this)});
		}
	}
};

/**
 * Class that creates a dynamic HTML popup. Supported options are:
 * 
 * 		bodyId: This can be the ID of an existing DIV, which will then be used as the body
 *				of the popup, or of a non-existing DIV, which will then be created, so you
 *				can add its contents completely dynamically.
 *
 *		containerClass: [cssClassName] (default: null): If specified, this CSS class is added
 *				to the popup_container div. Allows you to have separate styling for different popup
 *				instances.
 *
 *		titleClass: [cssClassName] (default: null): If specified, this CSS class is added
 *				to the title div. Allows you to have separate styling for different popup
 *				instances.
 *
 *		bodyClass: [cssClassName] (default: null): If specified, this CSS class is added
 *				to the body div. Allows you to have separate styling for different popup
 *				instances.
 *
 *		resizeClass: [cssClassName] (default: null): If specified, this CSS class is added
 *				to the resize handle div. Allows you to have separate styling for different popup
 *				instances.
 *
 *		draggingClass: [cssClassName] (default: "popup_container_dragging"): This CSS class is added
 *				to the container while the popup is being dragged. By default used to make the popup
 *				transparent while dragging.
 *
 *		resizingClass: [cssClassName] (default: "popup_container_resizing"): This CSS class is added
 *				to the container while the popup is being resized. By default used to make the popup
 *				transparent while dragging.
 *
 * 		closable: true | false (default: true)
 * 		closeHandler: [function]
 *		draggable: true | false (default: true)
 *		fading: true | false (default: true)
 *		fadeDuration: [seconds] (default: 0.3)
 *		modal: true | false (default: false)
 * 		position: "center" | "n" | "ne" | "e" | "se" | "s" | "sw" | "w" | "nw" |
 *				  "centerElement" | "nElement" | "neElement" | "eElement" | "seElement" | "sElement" | "swElement" | "wElement" | "nwElement" |
 * 				  "mouse" | [win-x,win-y] (default: "center")
 * 		positionOffset: [offset-x, offset-y] (default: [0,0])
 * 		positionElement: [id-or-element] (default: none): the element to position popup relative to (see position)
 * 		positionAlignX: "left" | "center" | "right" (default: "center"): how is the popup horizontally aligned relative to its positionElement
 * 		positionAlignY: "above" | "center" | "below" (default: "center"): how is the popup vertically aligned relative to its positionElement
 * 		setPosition: "first" | "always" (default: "first"): when to set the position.
 * 		fixedPosition: true | false (default: true)
 * 		trackPosition: true | false (default: false): only if position = "mouse": causes popup to keep following mouse pointer 
 *		width: null (defaults to CSS width of popup_container)
 *		height: null (defaults to natural height of popup_body)
 *		resizable: true | false (default: false)
 * 		allowResizeX: true (false if resizable=false),
 * 		allowResizeY: true (false if resizable=false),
 * 		minWidth: 100,
 * 		maxWidth: null,
 * 		minHeight: 40,
 * 		maxHeight: null,
 * 		minimizable: true,
 * 		minimizeHandler: [function]
 * 		shadow: true | false (default: true)
 * 		title: HTML string (default: null)
 * 		timeout: [seconds] (default: null): if set, popup is automatically hidden after the
 * 					specified number of seconds have passed, after show() has been called.
 * 
 * @param {Object} options Map with options. 
 */
Util.Popup = function(options) {
	this._added = false;

	this.options = {
		bodyId: null,
		closable: true,
		draggable: true,
		dragHandle: "title",
		draggingClass: "popup_container_dragging",
		fading: true,
		fadeDuration: 0.3,
		modal: true,
		position: "center",
		positionOffset: [0, 0],
		setPosition: "first",
		fixedPosition: true,
		trackPosition: false,
		positionAlignX: "center",
		positionAlignY: "center",
		resizable: true,
		resizingClass: "popup_container_resizing",
		allowResizeX: true,
		allowResizeY: true,
		minWidth: 200,
		minHeight: 0,
		minimizable: true,
		shadow: true
	};
	Object.extend(this.options, options || {});
	
	// IE6 doesn't support position: fixed, so reset if specified.
	if (Util.isIE6()) {
		if (this.options.fixedPosition) {
			this.options.fixedPosition = false;
		}		
	}
	
	var body = $(this.options.bodyId);
	if (body == null) {
		body = document.createElement("DIV");
		body.id = this.options.bodyId;
	}
	body.className = "popup_body" + (this.options.shadow ? " drop_shadow_div" : "");
	if (this.options.shadow) {
		Element.addClassName(body, "drop_shadow_div");
	}
	if (this.options.bodyClass) {
		Element.addClassName(body, this.options.bodyClass);
	}
	this.body = body;
	
	this.container = document.createElement("DIV");
	this.container.className = "popup_container";
	this.container.style.display = "none";
	
	var contentParent = this.container;
	if (this.options.shadow) {
		this.container.className = "popup_container drop_shadow";
	}
	if (this.options.containerClass) {
		Element.addClassName(this.container, this.options.containerClass);
	}
	
	this.title = document.createElement("DIV");
	this.title.className = "popup_title" + (this.options.shadow ? " drop_shadow_div" : "");
	if (this.options.titleClass) {
		Element.addClassName(this.title, this.options.titleClass);
	}

	contentParent.appendChild(this.title);
	contentParent.appendChild(this.body);
	
	this.titleText = document.createElement("SPAN");
	this.titleText.className = "popup_title_text";
	
	this.titleButtons = document.createElement("SPAN");
	this.titleButtons.className = "popup_title_buttons";
	
	this.title.appendChild(this.titleText);
	this.title.appendChild(this.titleButtons);
	
	if (this.options.title != null) {
		this.setTitle(this.options.title);
	}
	if (this.options.width != null) {
		this.container.style.width = this.options.width + "px";
	}
	if (this.options.height != null) {
		this.body.style.height = this.options.height + "px";
	}
	
	this.setMinimizable(this.options.minimizable);
	this.setClosable(this.options.closable);
	this.setDraggable(this.options.draggable);
	this.setResizable(this.options.resizable);
	this.updateTitleHeight(true);
};

Util.Popup.prototype = {
	/**
	 * Completely remove the dialog from the DOM, cleaning up all event
	 * listeners, etc.
	 */
	destroy: function() {
		this.hide();
		
		// Unlink from DOM.
		if (this.container) {
			this.container.parentNode.removeChild(this.container);
			this.container = null;
		}
		
		// Remove event listeners.
		// TODO...
		
		// Clear DOM references.
		this.body = null;
		this.title = null;
		this.titleText = null;
		this.titleButtons = null;
		this.closeButton = null;
		this.minimizeButton = null;
		this._resizeHandle = null;
	},

	/**
	 * Set the title of the popup.
	 * 
	 * @param {Object} title New title.
	 */
	setTitle: function(title) {
		this.titleText.innerHTML = title;	
	},
	
	/**
	 * Retrieve the title of the popup.
	 */
	getTitle: function() {
		return this.titleText.innerHTML;
	},
	
	/**
	 * Method to set the title height correctly after a change in one of the options
	 * title, closable, minimizable, draggable, dragHandle. 
	 */
	updateTitleHeight: function(fromConstructor) {
		if (this.options.title == null && !this.options.closable && !this.options.minimizable &&
				(this.options.dragHandle == "all" || this.options.draggable == false))  {
			Element.removeClassName(this.title, "popup_title");
			if (this.options.shadow) {
				Element.removeClassName(this.title, "drop_shadow_div");
			}
			Element.addClassName(this.title, "popup_title_empty");
		} else {
			Element.removeClassName(this.title, "popup_title_empty");
			Element.addClassName(this.title, "popup_title");
			if (this.options.shadow) {
				Element.addClassName(this.title, "drop_shadow_div");
			}
		}
	},
	
	/**
	 * Specify whether this popup is closable.
	 * Defaults to true.
	 * 
	 * @param {Object} closable True or false.
	 */
	setClosable: function(closable) {
		if (closable) {
			var button = document.createElement("A");
			button.className = "popup_button_close";
			button.href = "#";
			button.onclick = this.onCloseClick.bindAsEventListener(this);
			button.innerHTML = " X ";
			this.titleButtons.appendChild(button);
			this.closeButton = button;
			
		} else if (this.closeButton != null) {
			this.closeButton.onclick = Prototype.emptyFunction;
			this.closeButton.parentNode.removeChild(this.closeButton);
			this.closeButton = null;
		}
		this.options.closable = closable;
	},
	
	/**
	 * Return whether this popup is closable.
	 */
	isClosable: function() {
		return this.options.closable;
	},
	
	/**
	 * Called when close button is clicked.
	 * By defaults hides the popup, but this can be overridden by specifying
	 * a "closeHandler" in the options.
	 */
	onCloseClick: function() {
		try {
			if (this.options.closeHandler) {
				this.options.closeHandler(this);
			} else {
				this.hide();
			}
		} catch( ex ) {
			alert(ex.message);
		}
		return false;
	},	
	
	/**
	 * Specify whether this popup is minimizable.
	 * Defaults to true.
	 * 
	 * @param {Object} minimizable True or false.
	 */
	setMinimizable: function(minimizable) {
		if (minimizable) {
			var button = document.createElement("A");
			button.className = "popup_button_minimize";
			button.href = "#";
			button.onclick = this.onMinimizeClick.bindAsEventListener(this);
			button.innerHTML = " M ";
			this.titleButtons.appendChild(button);
			this.minimizeButton = button;
			
		} else if (this.minimizeButton != null) {
			this.minimizeButton.onclick = Prototype.emptyFunction;
			this.minimizeButton.parentNode.removeChild(this.minimizeButton);
			this.minimizeButton = null;
		}
		this.options.minimizable = minimizable;
	},	
	
	/**
	 * Called when close button is clicked.
	 * By defaults hides the popup, but this can be overridden by specifying
	 * a "closeHandler" in the options.
	 */
	onMinimizeClick: function() {
		try {
			if (this.options.minimizeHandler) {
				this.options.minimizeHandler(this);
			} else {
				if (this.isMinimized()) {
					this.maximize();
				} else {
					this.minimize();
				}
			}
		} catch( ex ) {
			alert(ex.message);
		}
		return false;
	},
	
	/**
	 * Return whether this popup is draggable.
	 */
	isMinimizable: function() {
		return this.options.minimizable;
	},
	
	/**
	 * Specify whether this popup is draggable.
	 * Defaults to true.
	 * 
	 * @param {Object} draggable True or false.
	 */
	setDraggable: function(draggable) {
		if (draggable) {
			var dragHandle = this._getDragHandle();
			dragHandle.style.cursor = "move";
			this._dragStartHandler = this._dragStart.bindAsEventListener(this);
			Event.observe(dragHandle, "mousedown", this._dragStartHandler);
			this._dragging = false;
			
		} else if (this._dragStartHandler != null) {
			var dragHandle = this._getDragHandle();
			dragHandle.style.cursor = "default";
			Event.stopObserving(dragHandle, "mousedown", this._dragStartHandler);
			this._dragStartHandler = null;
		}
		this.options.draggable = draggable;
	},
	
	/**
	 * Return whether this popup is draggable.
	 */
	isDraggable: function() {
		return this.options.draggable;
	},
	
	_getDragHandle: function() {
		switch( this.options.dragHandle ) {
			case "all":
				return this.container;
				
			case "title":
			default:
				return this.title;
		}
	},
	
	_dragStart: function(e) {
		if (!this._dragging) {
			if (this.options.draggingClass) {
				Element.addClassName(this.container, this.options.draggingClass);
			}
			
			Event.stop(e);
			this._dragging = true;
			
			var pos = Event.pointer(e);
			this._lastDragX = pos.x;
			this._lastDragY = pos.y;
			
			this._dragMoveListener = this._dragMove.bindAsEventListener(this);
			this._dragEndListener = this._dragEnd.bindAsEventListener(this);
			Event.observe(document, "mousemove", this._dragMoveListener);
			Event.observe(document, "mouseup", this._dragEndListener);
		}
	},
	
	_dragMove: function(e) {
		Event.stop(e);

		var pos = Event.pointer(e);
	    var nextDragX = pos.x;
	    var nextDragY = pos.y;
	    var dx = nextDragX - this._lastDragX;
	    var dy = nextDragY - this._lastDragY;
	    this._lastDragX = nextDragX;
	    this._lastDragY = nextDragY;
	    
		Util.moveElement(this.container, dx, dy);
	},
	
	_dragEnd: function(e) {
		if (this._dragging) {
			if (this.options.draggingClass) {
				Element.removeClassName(this.container, this.options.draggingClass);
			}
			
			Event.stop(e);
			Event.stopObserving(document, "mousemove", this._dragMoveListener);
			Event.stopObserving(document, "mouseup", this._dragEndListener);
			this._dragMoveListener = null;
			this._dragEndListener = null;
			this._dragging = false;
		}
	},
	
	/**
	 * Specify whether this popup is resizable.
	 * Defaults to false.
	 * 
	 * @param {Object} resizable True or false.
	 */
	setResizable: function(resizable) {
		if (resizable) {
			var handle = this._resizeHandle;
			if (handle == null) {
				handle = document.createElement("DIV");
				handle.className = "popup_resize_handle" + (this.options.shadow ? " drop_shadow_div" : "");
				if (this.options.resizeClass) {
					Element.addClassName(handle, this.options.resizeClass);
				}
				
				if (this.options.allowResizeX && this.options.allowResizeY) {
					handle.style.cursor = "se-resize";
				} else if (this.options.allowResizeX) {
					handle.style.cursor = "e-resize";
				} else {
					handle.style.cursor = "s-resize";
				}
				
				this._resizeHandle = handle;
				this.container.appendChild(handle);
			}
			
			this._resizeStartHandler = this._resizeStart.bindAsEventListener(this);
			Event.observe(handle, "mousedown", this._resizeStartHandler);
			this._resizing = false;
			
		} else if (this._resizeHandle != null) {
			Event.stopObserving(this.body, "mousedown", this._resizeStartHandler);
			this._resizeStartHandler = null;
			
			var handle = this._resizeHandle;
			if (handle != null) {
				handle.parentNode.removeChild(handle);
				this._resizeHandle = null;
			}
		}
		this.options.resizable = resizable;
	},
	
	/**
	 * Return whether this popup is resizable.
	 */
	isResizable: function() {
		return this.options.resizable &&
			(this.options.allowResizeX == null || this.options.allowResizeX) &&
			(this.options.allowResizeY == null || this.options.allowResizeY);
	},
	
	_resizeStart: function(e) {
		if (!this._resizing) {
			if (this.options.resizingClass) {
				Element.addClassName(this.container, this.options.resizingClass);
			}
			
			Event.stop(e);
			
			var pos = Event.pointer(e);
			this._resizing = true;
			this._lastResizeX = pos.x;
			this._lastResizeY = pos.yl
			
			this._resizeMoveListener = this._resizeMove.bindAsEventListener(this);
			this._resizeEndListener = this._resizeEnd.bindAsEventListener(this);
			Event.observe(document, "mousemove", this._resizeMoveListener);
			Event.observe(document, "mouseup", this._resizeEndListener);
		}
	},
	
	_resizeMove: function(e) {
		Event.stop(e);
		
		var pos = Event.pointer(e);
	    var nextResizeX = pos.x;
	    var nextResizeY = pos.y;
	    var dx = nextResizeX - this._lastResizeX;
	    var dy = nextResizeY - this._lastResizeY;
	    this._lastResizeX = nextResizeX;
	    this._lastResizeY = nextResizeY;
	    
		if (this.options.allowResizeX == null || this.options.allowResizeX) {
			var newWidth = parseInt(Element.getStyle(this.container, "width")) + dx;
			if (newWidth >= this.options.minWidth && (this.options.maxWidth == null || newWidth <= this.options.maxWidth)) {
				this.container.style.width = newWidth + "px";
			}
		}
		
		if (this.options.allowResizeY == null || this.options.allowResizeY) {
			var newHeight = parseInt(Element.getStyle(this.body, "height")) + dy;
			if (newHeight >= this.options.minHeight && (this.options.maxHeight == null || newHeight <= this.options.maxHeight)) {
				this.body.style.height = newHeight + "px";
			}
		}		
	},
	
	_resizeEnd: function(e) {
		if (this._resizing) {
			if (this.options.resizingClass) {
				Element.removeClassName(this.container, this.options.resizingClass);
			}
						
			Event.stop(e);
			Event.stopObserving(document, "mousemove", this._resizeMoveListener);
			Event.stopObserving(document, "mouseup", this._resizeEndListener);
			this._resizeMoveListener = null;
			this._resizeEndListener = null;
			this._resizing = false;
		}
	},
	
	/**
	 * Set the position of the popup. Either the left/top coordinates of the popup can be specified
	 * directly, or if no arguments are given, the default position set, according to the position,
	 * positionOffset, positionElement, positionAlignX, positionAlignY, setPosition and fixedPosition
	 * options.
	 * 
	 * @param {Number} left Absolute x coordinate of the popup inside browser window.
	 * @param {Number} top Absolute y coordinate of the popup inside browser window.
	 */
	setPosition: function(left, top) {
		if (arguments.length == 2) {
			this.container.style.left = left + "px";
			this.container.style.top = top + "px";
			
		} else {
			if (this.options.fixedPosition) {
				this.container.style.position = "fixed";
			}
			
			// If position is one of the border positions, reset the alignments to sensible values, or else
			// the popup will be outside the visible window.
			switch( this.options.position ) {
				case "n":
					this.options.positionAlignX = "center";
					this.options.positionAlignY = "below";
					break;
				case "ne":
					this.options.positionAlignX = "left";
					this.options.positionAlignY = "below";
					break;
				case "e":
					this.options.positionAlignX = "left";
					this.options.positionAlignY = "center";
					break;
				case "se":
					this.options.positionAlignX = "left";
					this.options.positionAlignY = "above";
					break;
				case "s":
					this.options.positionAlignX = "center";
					this.options.positionAlignY = "above";
					break;
				case "sw":
					this.options.positionAlignX = "right";
					this.options.positionAlignY = "above";
					break;
				case "w":
					this.options.positionAlignX = "right";
					this.options.positionAlignY = "center";
					break;
				case "nw": 
					this.options.positionAlignX = "right";
					this.options.positionAlignY = "below";
					break;
			}
				
			var offset = this.options.positionOffset || [0, 0];
			switch( this.options.position ) {
				case "center": {
					var pos = Util.centerOverVisible(Element.getWidth(this.container), Element.getHeight(this.container), true);
					this.setPosition(pos[0] + offset[0], pos[1] + offset[1]);
					break;
				}
					
				case "n":
				case "ne":
				case "e":
				case "se":
				case "s":
				case "sw":
				case "w":
				case "nw": {
					var isNorth = this.options.position.charAt(0) == "n";
					var isSouth = this.options.position.charAt(0) == "s";
					var isWest = this.options.position.charAt(0) == "w" || this.options.position.charAt(1) == "w";
					var isEast = this.options.position.charAt(0) == "e" || this.options.position.charAt(1) == "e";
					
					var elemPos = [0, 0]; // The left top of the browser window.
					var elemSize = Util.getWindowSize(); // The visible part of the browser window.
					var popupSize = Element.getDimensions(this.container);
					var popupPos = this._alignPopup(elemPos, {width: elemSize[0], height: elemSize[1]}, popupSize, isNorth, isEast, isSouth, isWest);								
					this.setPosition(popupPos[0] + offset[0], popupPos[1] + offset[1]);
					break;
				}
					
				case "nElement":
				case "neElement":
				case "eElement":
				case "seElement":
				case "sElement":
				case "swElement":
				case "wElement":
				case "nwElement": {
					var isNorth = this.options.position.charAt(0) == "n";
					var isSouth = this.options.position.charAt(0) == "s";
					var isWest = this.options.position.charAt(0) == "w" || this.options.position.charAt(1) == "w";
					var isEast = this.options.position.charAt(0) == "e" || this.options.position.charAt(1) == "e";
					
					var elem = $(this.options.positionElement);
					if (elem) {
						var elemPos = Element.cumulativeOffset(elem);
						var elemSize = Element.getDimensions(elem);
						var popupSize = Element.getDimensions(this.container);
						var popupPos = this._alignPopup(elemPos, elemSize, popupSize, isNorth, isEast, isSouth, isWest);
						
						this.setPosition(popupPos[0] + offset[0], popupPos[1] + offset[1]);
					} 
					break;
				}
				
				case "mouse": {
					var popupSize = Element.getDimensions(this.container);
					var popupPos = this._alignPopup([Util._pointerX, Util._pointerY], {width: 0, height: 0}, popupSize);								
					this.setPosition(popupPos[0] + offset[0], popupPos[1] + offset[1]);
					break;
				}
					
				default:
					if (typeof this.options.position == "object") {
						this.setPosition(this.options.position[0] + offset[0],
										 this.options.position[1] + offset[1]);
					}
					break;
			}
		}
	},
	
	_alignPopup: function(elemPos, elemSize, popupSize, isNorth, isEast, isSouth, isWest) {
		var left = elemPos[0];
		var top = elemPos[1];
		var elemWidth = elemSize.width;
		var elemHeight = elemSize.height;
		var popupWidth = popupSize.width;
		var popupHeight = popupSize.height;
		
		// Large ugly if-statements to apply specified position and alignment options.
		var calculateLeftOffset = function(alignX) {
			if (isWest) {
				if (alignX == "left") {
					return -popupWidth;
				} else if (alignX == "right") {
					return 0;
				} else {
					return -popupWidth / 2;
				}
			} else if (isEast) {
				if (alignX == "left") {
					return elemWidth - popupWidth;
				} else if (alignX == "right") {
					return elemWidth;								
				} else {
					return -(elemWidth - popupWidth / 2);
				}
			} else {
				if (alignX == "left") {
					return elemWidth / 2 - popupWidth;								
				} else if (alignX == "right") {
					return elemWidth / 2;
				} else {
					return (elemWidth - popupWidth) / 2;
				}
			}							
		}
		
		if (isSouth) {
			left += calculateLeftOffset(this.options.positionAlignX);
			if (this.options.positionAlignY == "above") {
				top += elemHeight - popupHeight;
			} else if (this.options.positionAlignY == "below") {
				top += elemHeight;
			} else {
				top += elemHeight - popupHeight / 2;
			}
		} else if (isNorth){
			left += calculateLeftOffset(this.options.positionAlignX);
			if (this.options.positionAlignY == "above") {
				top -= popupHeight;
			} else if (this.options.positionAlignY == "below") {
				top += 0;
			} else {
				top -= popupHeight / 2;
			}
		} else {
			left += calculateLeftOffset(this.options.positionAlignX);
			if (this.options.positionAlignY == "above") {
				top += elemHeight / 2 - popupHeight;
			} else if (this.options.positionAlignY == "below") {
				top += elemHeight / 2;
			} else {
				top += (elemHeight - popupHeight) / 2;
			}
		}
		return [left, top];
	},
	
	/**
	 * Show or hide the popup.
	 * @param {Boolean} show If specified, the value is used to hide or show the popup: true to show, false to hide.
	 * 						 If not specified, the popup is shown.
	 */
	show: function(show) {
		var isFirstTime = false;
		if (!this._added) {
			this._added = true;
			document.body.appendChild(this.container);
			this.body.style.display = "";
			isFirstTime = true;
		}
		if (show == null || show) {
			if (this.options.modal) {
				Util.blockWindow(this.options);
			}

			if (this.options.setPosition == "always" || isFirstTime) {
				this.setPosition();
			}

			if (isFirstTime) {
				// IE fix when shadow is used: left part of the divs in the container is cropped.
				// To fix this, we must give the container layout by settings its height (or one of
				// several other properties). So retrieve the height after the popup is on screen,
				// and set it.
				if (Util.isIE6() && this.options.shadow) {
					// Only done for IE.
					var body = this.body;
					window.setTimeout(function() {
						var height = parseInt(Element.getStyle(body, "height"));
						body.style.height = height + "px";
						//window.status = "body height: " + height;
					}, 1000);
				}
			}

			if (this.options.fading) {
				Effect.Appear(this.container, {	duration: this.options.fadeDuration	});
			} else {
				Element.show(this.container);
			}
			
			if (this.options.timeout != null) {
				window.setTimeout(function() {
					this.hide();
				}.bind(this), this.options.timeout * 1000);
			}
		} else {
			this.hide();
		}
	},
	
	/**
	 * Hide the popup.
	 */
	hide: function() {
		if (this.options.fading) {
			Effect.Fade(this.container, { duration: this.options.fadeDuration });
		} else {
			Element.hide(this.container);						
		}
		if (this.options.modal) {
			Util.unblockWindow(this.options);
		}
	},
	
	/**
	 * Return whether the popup is currently visible.
	 */
	isVisible: function() {
		return this._added && Element.visible(this.container);
	},

	/**
	 * Minimize the popup. By default this hides the body and resize handle.
	 */	
	minimize: function() {
		if (!this._minimized) {
			if (this.options.fading) {
				Effect.SlideUp(this.body, { duration: this.options.fadeDuration, afterFinish: function() {
					if (this._resizeHandle) {
						Effect.Fade(this._resizeHandle, { duration: this.options.fadeDuration });
					}
				}.bind(this)});
			} else {
				Element.hide(this.body);
				if (this._resizeHandle) {
					Element.hide(this._resizeHandle);
				}
			}
			this._minimized = true;
		}
	},
	
	/**
	 * Maximize the popup. By default this show the body and resize handle.
	 */	
	maximize: function() {
		if (this._minimized) {
			if (this.options.fading) {
				Effect.SlideDown(this.body, { duration: this.options.fadeDuration, afterFinish: function() {
					if (this._resizeHandle) {
						Effect.Appear(this._resizeHandle, { duration: this.options.fadeDuration });
					}
				}.bind(this)});
			} else {
				Element.show(this.body);
				if (this._resizeHandle) {
					Element.show(this._resizeHandle);
				}
			}
			this._minimized = false;
		}		
	},
	
	/**
	 * Return whether the popup is currently minimized.
	 */
	isMinimized: function() {
		return this._minimized;
	}
};

Event.observe(window, "load", Util._onLoad.bind(Util));
Event.observe(window, "unload", Util._onUnLoad.bind(Util));
