/**
 *
 */


/*
 * Extend the data prototype to allow casting to unix timestamps.
 *
 * This is useful for communicating with PHP/MySQL
 */
Date.prototype.toUnixTimestamp = function() {
	// convert to unix timestamp
	return Math.round(this.valueOf()/1000);
}

/**
 * Provides miscellaneous functionality for SiteCam but may be used for other
 * projects as well. This class need not be initiated.
 *
 * SiteCam might need to be initialized by calling setSessionInfo, this will
 * allow a session to be resumed within API calls, even when the user rejected
 * our session cookie.
 */
var SiteCam = {
	useCookie: true,
	sessionName: "",
	sessionId: "",
	errorHandler: null,
	/**
	 * Formats an API URL
	 *
	 * @param module The name of the module
	 * @param cid The Base64 encoded camera id
	 * @param command The command to send
	 * @param params An hash of parameters and their corresponding values
	 */
	_getUrl: function(module, camera, command, params) {
		var session = this.useCookie ? '' 
			: '&' + this.sessionName + '=' + this.sessionId;
			
		return 'api.php?module=' + module + 
				'&cid=' + camera +
				'&command=' + command +
				'&' + params.toQueryString() +
				session;
	},
	/**
	 * Executes an api call using XMLHttpRequest. It will send a request to the
	 * api.php script which in turn will execute the api_command method of the
	 * corresponding module.
	 *
	 * @param module The name of the module
	 * @param camera The encoded camera id
	 * @param command The command to execute
	 * @param additional parameters as (param: value) pairs
	 * @param function (optional) function to call on success
	 */
	apiCall: function(module, camera, command, params) {
		var options = {onFailure: this.defaultErrorHandler.bind(this)};
		if (arguments.length > 4) {
			options = {
				onFailure: this.defaultErrorHandler.bind(this),
				onSuccess: arguments[4]
			};
		}
		new Ajax.Request(
			this._getUrl(module, camera, command, params),
			options
		);
	},
	/**
	 * Updates an element using the output of an API Call.
	 *
	 * @param element The id of the element to update
	 * @param module The name of the module
	 * @param camera The encoded camera id
	 * @param command The command to execute
	 * @param params additional parameters as (param: value) pairs
	 */
	updateFromApi: function(element, module, camera, command, params) {
		var url = this._getUrl(module, camera, command, params);
		new Ajax.Updater($(element), url);
	},
	/**
	 * Sets the session information for API calls. SiteCam will automatically
	 * append sessionName=sessionId to query part of the API request. The api
	 * script can then use this information to resume the client's session.
	 *
	 * @param useCookie Should be true if the user's client accepted our
	 *	session cookie
	 * @param sessionName The name of the session
	 * @param sessionId the ID of the session
	 */
	setSessionInfo: function(useCookie, sessionName, sessionId) {
		this.useCookie = useCookie;
		if (!this.useCookie) {
			this.sessionName = sessionName;
			this.sessionId = sessionId;
		}
	},
	/**
	 * Executes the erorr handler. This handler should be a
	 * function that accept one parameter, namely the content of the error.
	 *
	 * If no error handler is set, or null is passed, an alert will pop-up when
	 * an error occurs
	 *
	 * @param response The function to execute upon an error
	 */
	defaultErrorHandler: function(response) {
		if (this.errorHandler == null)
			alert(response.responseText);
		else
			this.errorHandler(response.responseText);
	},
	/**
	 * Sets an error handler that will be executed if an API call fails. The
	 * function should accept a parameter, namely the content of the error.
	 *
	 * If no error handler is set, or null is passed, an alert will pop-up when
	 * an error occurs
	 *
	 * @param response The function to execute upon an error
	 */
	setErrorHandler: function(fun) {
		this.errorHandler = fun;
	}
};

/**
 * LoadImages loads multiple images in the background and shows
 * them all simultaneously once all images all processes have been
 * loaded
 */
var LoadImages = Class.create();
LoadImages.prototype = {
	//buffers: null,
	//listener: null,
	//images: null,
	//done:  0,
	//todo: 0,
	//hidden: 0,
	//duration: 0.5,
	//showing: false,
	//finishedListener: null,
	/**
	 * Constructor.
	 *
	 * @param sources An array of url's to the images to be loaded
	 * @param images An array of image
	 */
	initialize: function(sources, images) {
		this.listener = this.bufferLoaded.bindAsEventListener(this);
		this.todo = sources.length;
		this.buffers = new Array(this.todo);
		this.images = images;
		this.done = 0;
		this.hidden = 0;
		this.duration = 0.5;
		this.showing = false;
		this.finishedListener = null;
		this.hideOld();
		for(var i = 0; i < this.todo; ++i) {
			var buffer = $(new Image());
			var container = this.images[i].up('div');
			container.setStyle({
				width: container.getWidth() + 'px',
				height: container.getHeight() + 'px'
			});
			this.buffers[i] = buffer;
			Event.observe(this.buffers[i], 'load', this.listener);
			this.buffers[i].src = sources[i];
		}
	},
	/*
	 * Sets the listener to be executed once all images have been loaded
	 */
	setFinishedListener: function(func) {
		this.finishedListener = func;
	},
	/*
	 * PRIVATE METHOD
	 *
	 * Called after an image was successfully loaded
	 */
	bufferLoaded: function(event) {
		++this.done;
		if (this.done >= this.todo) {
			if (this.hidden == this.done) {
				showNew();
			}
		}
	},
	/**
	 * PRIVATE METHOD
	 *
	 * Hides the old images using an animation
	 */
	hideOld: function() {
		this.done = 0;
		for(var i = 0; i < this.todo; ++i) {
			new Effect.Shrink(this.images[i], {
				'duration': this.duration,
				afterFinish: this.imageHidden.bind(this)
			});
		}
	},
	/**
	 * PRIVATE METHOD
	 *
	 * Called when an image was hidden
	 */
	imageHidden: function () {
		++this.hidden;
		if (this.hidden >= this.todo) {
			if (this.hidden == this.todo)
				this.showNew();
		}
	},
	/**
	 * PRIVATE METHOD
	 *
	 * Shows the newly loaded images
	 */
	showNew: function() {
		if (this.showing)
			return;
		this.showing = true;
		
		for(var i = 0; i < this.todo; ++i) {
			this.images[i].src = this.buffers[i].src;
			new Effect.Grow(
				this.images[i], {
					'duration': this.duration,
					afterFinish: this.finishedListener
				}
			);
			Event.stopObserving(this.buffers[i], 'load', this.listener);
			this.buffers[i] = null;
		}
		
		//if (this.finishedListener != null) {
		//	this.finishedListener();
		//}
	}
};

var RefreshImage = Class.create({

	initialize: function(img, interval) {
		this.timer = null;
		this.img = $(img);
		this.defaultSrc = this.img.src;
		this.interval = interval;
		this.seq = 1;
		this.buffer = $(new Image());
        //this.bufferDocument = this.createDocumentElement();
		this.lastDisplay = new Date();
		this.generatePeriodicalExecuter();
		this.loadImageFunc = null;
		this.buffer = null;
	},
	
	generatePeriodicalExecuter: function() {
		//new PeriodicalExecuter(this.loadImage.bind(this), this.interval/1000);
		if (this.loadImageFunc == null) {
			this.loadImageFunc = this.loadImage.bind(this);
		}
		this.timer = setTimeout(this.loadImageFunc, this.interval);
	},
	
    getDocumentElementId: function() {
        return 'refreshimage_buffer_'+this.img.id;
    },
    
	loadImage: function() {
		//pe.stop();
		this.lastUpdate = new Date();
		this.timer = null;
		//this.buffer = null;
		this.buffer = this.generateImage();
	},
	generateImage: function() {
        var tmp = new Image();
		var separator = '?';
		++this.seq;
		if (this.defaultSrc.indexOf('?') >= 0)
			separator = '&';
        Event.observe(
			tmp,
			'load',
			this.display.bindAsEventListener(this)
		);
        Event.observe(
            tmp,
            'error',
            this.errorHandler.bindAsEventListener(this)
        );
		tmp.src = this.defaultSrc + separator + "seq=" + this.seq;
        return tmp;
	},
	display: function() {
		//window.clearTimeout(this.timer);
        var element = this.buffer;
		this.img.src = element.src;
		this.buffer = null;
		this.lastDisplay = new Date();
		this.generatePeriodicalExecuter();
		//this.loadImage();
	},
    errorHandler: function(e) {
        //this.loadImage();
		this.generatePeriodicalExecuter();
    },
	imageLoaded: function(e) {
    	this.display();
		/*var now = new Date();
		var dif = now.getTime() - this.lastUpdate.getTime();
		if (dif >= this.interval) {
			this.timer = null;
			this.display();
		}
		else {
			this.timer = window.setTimeout(this.display.bind(this), this.interval - dif);
		}*/
	}
});

/**
 * Loads an image in the background. Once the image is loaded it automatically
 * displays the loaded image.
 *
 * Use as follows: new LoadImage('image.jpg', $('element'))
 */
var LoadImage = Class.create();
LoadImage.prototype = {
	showNew: function() {
		this.image.src = this.buffer.src;
		if (this.animate)
			new Effect.Grow(this.image, {'duration': this.duration});
		this.cleanUp();
	},
	bufferLoaded: function(event) {
		if (this.animate) {
			new Effect.Shrink(this.image, {
				'duration': this.duration,
				afterFinish: this.showNew.bind(this)
			});
		}
		else {
			this.showNew();
		}
	},
	/**
	 * Constructor
	 *
	 * Starts loading an image from src. As soon as it is loaded, the img-
	 */
	initialize: function(src, image, animate) {

		this.buffer = null;
		this.image = null;
		this.container = null;
		this.duration = 0.5;
		this.eventHandler = null;
		this.animate = false;
		if (arguments.length >= 3)
			this.animate = animate;

		this.image = image;
		this.container = this.image.up('div');
		this.container.setStyle({
			width: this.container.getWidth() + 'px',
			height: this.container.getHeight() + 'px'
		});
		this.buffer = $(new Image());
		//this.buffer = new Image();
		this.eventHandler = this.bufferLoaded.bindAsEventListener(this);
		Event.observe(
			this.buffer,
			'load',
			this.eventHandler
		);
		this.buffer.src = src;
	},
	cleanUp: function() {
		Event.stopObserving(this.buffer, 'load', this.eventHandler);
		this.buffer = null;
	}
};

var DateBar = Class.create();
DateBar.prototype = {
	resolution: 'month', // the resolution of the DateBar, either year, month or week
	container: null, // the container of scrollDiv
	scrollDiv: null, // the div that will scroll 
	fromDate: null, // the first date in the time span
	toDate: null, // the last date of the time span
	currentDate: null, // the currently selected date
	moving: false, // will only change date if the current bar is not moving
	containerWidth: 0, // the width of the container
	dateChangeListener: null,
	initialize: function(
			container, 
			cWidth, 
			cHeight, 
			elementWidth, 
			fromDate,
			toDate,
			currentDate,
			resolution
	) {
		if ((resolution == 'month') 
		||  (resolution == 'year')
		||  (resolution == 'week')
		||  (resolution == 'day')) {
			this.resolution = resolution;
		}
		
		this.elementWidth = elementWidth;
		
		this.container = $(container);
		this.container.makeClipping().makePositioned();
		this.container.setStyle({width: cWidth + 'px', height: cHeight + 'px'});
		this.containerWidth = cWidth;
		this.scrollDiv = this.container.down();
		
		this.setTimeSpan(fromDate, toDate);
		this.currentDate = this.fromDate;
		this.createDateDivs();
		
		this.gotoDate(currentDate);
	},
	setDateChangeListener: function(func) {
		this.dateChangeListener = func;
	},
	gotoDate: function(newDate) {
		// round the new date
		newDate = this.roundDate(newDate);
		if (newDate == this.currentDate)
			return;
		
		// calculate difference
		var diff =  newDate - this.currentDate;
		
		// calculate difference in units 
		var unitLength;
		switch(this.resolution) {
			case 'day':
				unitLength = 24 * 3600 * 1000;
				break;
			case 'week':
				unitLength = 7 * 24 * 3600 * 1000; // one week
				break;
			case 'month':
				unitLength = 31 * 24 * 3600 * 1000; // one month approx
				break;
			case 'year':
				unitLength = 366 * 24 * 3600 * 1000; // one year approx
				break;
		}
		
		// this code was not daylight savings time proof
		/*var units = (diff > 0) ?
				Math.ceil(diff / unitLength) :
				Math.floor(diff / unitLength);
		*/
		// this line is DST proof!
		var units = Math.round(diff / unitLength);
		 
		this.currentDate = newDate;
		new Effect.Move(this.scrollDiv, {
			x: -units*this.elementWidth, 
			y: 0, 
			mode: 'relative',
			queue: {position: 'end', scope: 'DateBarQueue', limit: 1},
			beforeStart: this.startMoving.bind(this),
			afterFinish: this.stopMoving.bind(this)
		});
	},
	startMoving: function() {
		this.moving = true;
	},
	stopMoving: function() {
		this.moving = false;
	},
	dateBoxClicked: function(event, date) {
		if (this.moving)
			return;
		if (this.dateChangeListener != null)
			this.dateChangeListener(date);
		this.gotoDate(date);
	},
	/**
	 * Returns a new date object that is rounded downwards to the correct
	 * resolution
	 */ 
	roundDate: function(date) {
		switch(this.resolution) {
			case 'day':
				var d = new Date(
					date.getFullYear(), 
					date.getMonth(), 
					date.getDate(),
					0,
					0,
					0,
					0
				);
				return d;
			case 'week':
				var d = new Date(
					date.getFullYear(), 
					date.getMonth(), 
					date.getDate(),
					0,
					0,
					0,
					0
				);
				// go to the sunday of that week
				d.setDate(d.getDate() - d.getDay());
				return d;
			case 'month':
				return new Date(
					date.getFullYear(),
					date.getMonth(),
					1,
					0,
					0,
					0,
					0
				);
				break;
			case 'year':
				return new Date(
					date.getFullYear(),
					0,
					1,
					0,
					0,
					0,
					0
				);
				break;
		}
	},
	setTimeSpan: function(fromDate, toDate) {
		this.fromDate = this.roundDate(fromDate);
		this.toDate = this.roundDate(toDate);
	},
	createDateDivs: function() {
		// create start marker
		var startBox = $(document.createElement('div'));
		var lft = 0;
		startBox.addClassName('date_box_start_marker');
		startBox.setStyle({
			height: this.elementWidth + 'px', 
			width: this.elementWidth + 'px',
			position: 'absolute',
			top: '0px',
			left: lft + 'px'
			//float: 'left'
		});
		startBox.makePositioned();
		this.scrollDiv.appendChild(startBox);
		
		
		var date = this.fromDate;
		this.scrollDiv.setStyle({height: this.elementWidth + 'px'});
		while(date <= this.toDate) {
			lft += this.elementWidth;
			var dateBox = this.createDateBox(date, lft);
			date = this.nextDate(date);
		}
		
		// create end marker
		var endBox = $(document.createElement('div'));
		endBox.addClassName('date_box_end_marker');
		endBox.setStyle({
			height: this.elementWidth + 'px', 
			width: this.elementWidth + 'px',
			position: 'absolute',
			left: (lft + this.elementWidth) + 'px',
			top: '0px'
		});
		endBox.makePositioned();
		this.scrollDiv.appendChild(endBox);
		
		// position the scrolling div
		var center = (this.containerWidth / 2 - this.elementWidth / 2);
		// substract this.elementWidth from center as the start marker should
		// not be centered
		lft = center - this.elementWidth;
		
		var cnt = this.scrollDiv.immediateDescendants().length;
		this.scrollDiv.setStyle({
			height: this.elementWidth + 'px',
			width: cnt * this.elementWidth + 'px',
			position: 'absolute',
			left: lft + 'px'
		});
		this.scrollDiv.makePositioned();
		
		var marker = $(document.createElement('div'));
		lft = center;
		marker.setStyle({
			width: this.elementWidth + 'px',
			height: this.elementWidth + 'px',
			position: 'absolute',
			left: lft + 'px',
			top: '0px',
			zIndex: '1',
			backgroundColor: 'red',
			opacity: '0.5'
		});
		this.container.appendChild(marker);
	},
	createDateBox: function(date, lft) {
		var dateBox = $(document.createElement('div'));
		dateBox.innerHTML = this.dateToCaption(date);
		this.scrollDiv.appendChild(dateBox);
		dateBox.makePositioned();
		dateBox.setStyle({
			height: this.elementWidth + 'px', 
			width: this.elementWidth + 'px',
		//	float: 'left'
			position: 'absolute',
			top: '0px',
			left: lft + 'px'
		});
		dateBox.addClassName('date_box_element');
		Event.observe(
			dateBox, 
			'click', 
			this.dateBoxClicked.bindAsEventListener(
				this, date)
		);
	},
	dateToCaption: function(date) {
		var caption = '';
		switch(this.resolution) {
			case 'day':
			case 'week':
				caption = caption + date.getDate() + '<br />';
			case 'month':
				caption = caption + (date.getMonth()+1) + '<br />';
			case 'year':
				caption = caption + date.getFullYear();
		}
		return caption;
	},
	nextDate: function(date) {
		var result = new Date(date.getFullYear(), 
			date.getMonth(), 
			date.getDate()
		);
		switch(this.resolution) {
			case 'day':
				result.setDate(result.getDate()+1);
				break;
			case 'week':
				result.setDate(result.getDate()+7);
				break;
			case 'year':
				result.setFullYear(result.getFullYear()+1);
				break;
			case 'month':
				result.setMonth(result.getMonth()+1);
				break;
		}
		return result;
	},
	prevDate: function(date) {
		var result = new Date(date.getFullYear(), 
			date.getMonth(), 
			date.getDate()
		);
		switch(this.resolution) {
			case 'day':
				result.setDate(result.getDate()-1);
				break;
			case 'week':
				result.setDate(result.getDate()-7);
				break;
			case 'year':
				result.setFullYear(result.getFullYear()-1);
				break;
			case 'month':
				result.setMonth(result.getMonth()-1);
				break;
		}
		return result;
	}
}


var ScrollContent = Class.create();
ScrollContent.prototype = {
	container: null,
	scrollElements: null,
	scrollDiv: null,
	elementWidth: 0,
 	moveDirection: '',
	initialize: function(container, cWidth, cHeight, elementWidth) {
		this.container = $(container);
		this.container.makeClipping().makePositioned();
		this.container.setStyle({width: cWidth + 'px', height: cHeight + 'px'});
		this.scrollDiv = this.container.down();
		this.scrollElements = this.scrollDiv.immediateDescendants();
		
		this.scrollDiv.setStyle({
			margin: '0px 0px 0px 0px',
			width: elementWidth * this.scrollElements.length + 'px', 
			height: cHeight
		});
		this.scrollDiv.makePositioned();
		
		
		this.scrollElements.invoke('setStyle', {
			margin: '0px 0px 0px 0px',
			padding: '0px 0px 0px 0px',
			width: elementWidth + 'px'
		});
		this.scrollElements.invoke('makePositioned');
		this.elementWidth = elementWidth;
	},
	getScrollX: function () {
		return Position.positionedOffset(this.scrollDiv)[0];
	},
	shouldMoveRight: function() {
		var xPos = this.getScrollX();
		var w = this.scrollDiv.getWidth();
		if (w+xPos <= this.container.getWidth())
			return false;
		return true;
		
	},
	shouldMoveLeft: function() {
		var xPos = this.getScrollX();
		if (xPos >= 0) 
			return false;
		return true;
	},
	moveLeft: function() {
		if (this.moveDirection != 'left') {
			this.moveDirection = 'left';
			this.move();
		}
	},
	moveRight: function() {
		if (this.moveDirection != 'right') {
			this.moveDirection = 'right';
			this.move();
		}
	},
	move: function() {
		if (this.moveDirection == '')
			return;
		
		var dx = this.elementWidth;
		if (this.moveDirection == 'left') {
			if (!this.shouldMoveLeft())
				return;
			dx = Math.min(dx, -this.getScrollX());
		} 
		else {
			if (!this.shouldMoveRight())
				return;
			dx *= -1;
		}
		
		// check if the scroll div is not already moving into that 
		// direction
		var queue = Effect.Queues.get('ScrollContent');
		var already_busy = queue.any(function(effect) {effect.x == dx});
		if (already_busy)
			return;
		
		this.moveEffect = new Effect.Move(this.scrollDiv, {
			x: dx, 
			y: 0, 
			mode: 'relative',
			queue: {position: 'end', scope: 'ScrollContent', limit: 1},
			afterFinish: this.move.bind(this)
		});
	},
	stopMoving: function() {
		this.moveDirection = '';
	}
};

var SiteCamLightbox = {
	lightbox: null,
	create: function() {
		if (this.lightbox == null) {
			this.lightbox = new Lightbox();
			window.myLightbox = this.lightbox;
		}
	},
	setBoundary: function(boundary) {
		if (this.lightbox == null)
			this.create();
		this.lightbox.setBoundary(boundary);
	},
	getLightbox: function() {
		return this.lightbox;
	}
};

var ModuleView = Class.create({
	initialize: function(name) {
		this.name = name;
	},
	getName: function() {
		return this.name;
	},
	tabActivated: function() {

	},
	tabDeactivated: function() {

	}
});

var SiteComponent = Class.create();
SiteComponent.prototype = {
	tabs: null,
	tabImagesLoading: null,
	tabLoaded: null,
	otherImages: 0,
	viewTab: null,
	activeTab: null,
	doneLoadingCallback: null,
	loaded: false,
	viewOnlyImagesLoadedCache: false,
	omnipresentImagesLoadedCache: false,
	/**
	 * Constructor
	 */
	initialize: function(doneLoadingCallback) {
		this.doneLoadingCallback = doneLoadingCallback;
	//	document.observe('dom:loaded', this.setup.bindAsEventListener(this));
	//},
	//setup: function() {
		// get the tabs
		this.tabs = $('tabs').descendants();
		this.moduleViews = new Array();
		// The first tab will be the active tab
		var firstTab = this.tabs[0];
		this.activeTab = firstTab;
		if (firstTab.hasClassName('view_module_tab')) {
			// the first tab isn't an actual view module
			this.viewTab = firstTab;
			this.showViewOnlyModules();// why did I every write hideViewOnlyModules(); here?
		}
		//Event.observe(this.activeTab, 'load',
		//	this.onViewReady.bindAsEventListener(this));
		// hide the the inactive tabbed modules
		this.tabLoaded = new Hash();
		for(var i = 1; i < this.tabs.length; ++i) {
			this.tabLoaded[this.tabs[i].id] = false;
			this.hideTabModule(this.tabs[i]);
		}
		
		this.activeTab.addClassName('active');
		var eventHandler = this.tabClicked.bindAsEventListener(this);
		this.tabs.each(function(tab) {
			Event.observe(tab, 'click', eventHandler);
		});
		SiteCamLightbox.create();

		this.monitorLoad();
	},
	monitorLoad: function() {
		var length = this.tabs.length;
		var tab = null;
		var loadingIcon = null;
		this.loaded = false;
		this.tabLoaded = new Hash();
		
		var isOpera = (navigator.userAgent.indexOf('Opera') > -1);
		if (isOpera) $$('img').each(function(img) {
			img.src = img.src + '#';
		});

		for(var i = 0; i < length; ++i) {
			tab = this.tabs[i];
			this.tabLoaded[tab.id] = false;
			if (length > 1) {
				loadingIcon = new Element('img', {
					'class': 'loading',
					'src': 'img/loading.gif'
				});
				tab.insert(loadingIcon);
			}
		}
		new PeriodicalExecuter(this.loadCheck.bind(this), 0.5);
	},
	unloadedImages: function(selector) {
		var tmp = $$(selector);
		return tmp.findAll(
			function(img) {
				return !img.complete;
			}
		);
	},
	viewOnlyImagesLoaded: function() {
		if (this.viewOnlyImagesLoadedCache)
			return true;
		var images = 
			this.unloadedImages('#view_only_modules img');
		this.viewOnlyImagesLoadedCache = (images.length == 0);
		return this.viewOnlyImagesLoadedCache;
	},
	omnipresentImagesLoaded: function() {
		if (this.omnipresentImagesLoadedCache)
			return true;
		var images =
			this.unloadedImages('#omnipresent img');
		this.omnipresentImagesLoadedCache = (images.length == 0);
		return this.omnipresentImagesLoadedCache;
	},
	tabImagesLoaded: function(tab) {
		var images =
			this.unloadedImages('div#'+this.getModuleName(tab)+' img');
		return (images.length == 0);
	},
	moduleImagesLoaded: function(module) {
		var images = this.unloadedImages('#'+module+' img');
		return (images.length == 0);
	},
	showTab: function(tab) {

		var loadingIcon = tab.getElementsBySelector('img.loading');
		this.tabLoaded[tab.id] = true;
		if (loadingIcon.length > 0) {
			loadingIcon = loadingIcon[0];
			loadingIcon.hide();
		}
		
		if (!this.loaded && this.activeTab == tab) {
			this.loaded = true;
			this.doneLoadingCallback();
			this.doneLoadingCallback = null;
		}
	},
	loadCheck: function(pe) {

		var length = this.tabs.length;
		// omnipresent images should be loaded anyway
		if (!this.omnipresentImagesLoaded())
			return;

		var viewOnlyLoaded = this.viewOnlyImagesLoaded();
		var tabsLoaded = 0;
		for(var i = 0; i < length; i++) {
			var tab = this.tabs[i];
			if (this.tabLoaded[tab.id]) {
				tabsLoaded++;
				continue;
			}
			if (this.tabImagesLoaded(tab)
			&& (i > 0 || viewOnlyLoaded)) {
				this.showTab(tab);
				tabsLoaded++;
			}
		}
		if (length == tabsLoaded) {
			pe.stop();
		}
	},
	setMaxViewTime: function(time) {
		new PeriodicalExecuter(
			this.maxViewTimeExceeded.bind(this), 
			time
		);
	},
	maxViewTimeExceeded: function(pe) {
		pe.stop();
		$$('body')[0].replace('<body>Bye</body>');
	},
	showViewOnlyModules: function() {
		$$('.view_only').invoke('show');
	},
	hideViewOnlyModules: function() {
		$$('.view_only').invoke('hide');
	},
	hideTabModule: function(tab) {
		var target = $(tab.id.sub('tab_', ''));
		target.hide();
	},
	showTabModule: function(tab) {
		var target = this.getTabDiv(tab);
		target.show();
	},
	getModuleName: function(tab) {
		return tab.id.sub('tab_', '');
	},
	getTabDiv: function(tab) {
		return $(this.getModuleName(tab));
	},
	registerModuleView: function(moduleView) {
		this.moduleViews.push(moduleView);
	},
    isEmpty: function(tab) {
        var tabDiv = this.getTabDiv(tab);
        return (tabDiv.select('script').length
            == tabDiv.childElements().length);
    },
	gotoTab: function(newTab) {

		if (this.activeTab == newTab // already looking at this module
		|| !this.tabLoaded[newTab.id]) { // tab not yet loaded
			return;
		}

        var i = 0;
        var name = '';
        var tabName = newTab.id.sub('tab_', '');

        if (this.isEmpty(newTab)) {
            // empty tabs are used for triggering certain actions
            for(i = 0; i < this.moduleViews.length; ++i) {
                name = this.moduleViews[i].getName();
                if (name == tabName) {
                    this.moduleViews[i].tabActivated();
                    return;
                }
            }
            // done handling this type of tabs
        }

		this.hideTabModule(this.activeTab);
		this.showTabModule(newTab);

		this.activeTab.removeClassName('active');
		newTab.addClassName('active');
		
		for(i = 0; i < this.moduleViews.length; ++i) {
			name = this.moduleViews[i].getName();
			if (name == tabName) {
				this.moduleViews[i].tabActivated();
			}
			else if (name ==
					this.activeTab.id.sub('tab_', '')) {
				this.moduleViews[i].tabDeactivated();
			}
		}
		this.activeTab = newTab;

		// check if the new tab is the view module and show view_only
		// modules accordingly
		if (newTab == this.viewTab) {
			this.showViewOnlyModules();
		}
		else {
			this.hideViewOnlyModules();
		}
	},
 	tabClicked: function(event) {
		var newTab = Event.element(event);
		this.gotoTab(newTab);
	}
};