var Waiter = new Class({
	options: {
		baseHref: '',
		img: {
			src: '../_images/ajax-loader.gif',
			id: 'waitingImg',
			styles: {
				position: 'absolute',
				width: 126,
				height: 22,
				display: 'none',
				opacity: 0,
				zIndex: 999
			}
		},
		imgPosition: {},
		layer:{
			id: 'waitingDiv',
			background: '#000000',
			opacity: 0.7
		},
		fxOptions: {}
	},
	
	initialize: function(target, options){
		this.target = $(document.body);
		this.setOptions(options);
		this.waiterImg = $(this.options.img.id) || new Element('img', $merge(this.options.img, {
			src: this.options.baseHref + this.options.img.src
		})).injectInside(document.body);
		this.waiterDiv = $(this.options.layer.id) || new Element('div', {
			id: this.options.layer.id,
			styles: {
				width: 0,
				height: 0,
				position: 'absolute',
				zIndex: 998,
				display: 'none',
				opacity: 0,
				backgroundColor: this.options.layer.background
			}
		}).injectInside(document.body);
		this.waiterFx = this.waiterFx || new Fx.Elements($$(this.waiterImg, this.waiterDiv), this.options.fxOptions);
	},

	toggle: function(element, show) {
		if (this.inTransit) {
			this.chain(this.toggle.bind(this, [element, show]));
			return this;
		}
		//the element or the default
		element = $(element) || $(this.active) || $(this.target);
		if (!$(element)) return this;
		if (this.active && element != this.active) return this.stop().chain(this.start.bind(this, element));
		//if it's not active or show is explicit
		//or show is not explicitly set to false
		//start the effect
		if((!this.active || show) && show !== false) this.start(element);
		//else if it's active and show isn't explicitly set to true
		//stop the effect
		else if(this.active && !show) this.stop();
		return this;
	},
	
	start: function(element){
		if (this.inTransit) {
			this.chain(this.start.bind(this, element));
			return this;
		}
		this.inTransit = true;
		element = $(element) || $(this.target);
		var start = function() {
			var dim = element.getComputedSize();
			this.active = element;
			this.waiterImg.setPosition($merge(this.options.imgPosition, {
				relativeTo: element
			})).show();
			
			this.waiterDiv.setStyles({
				width: dim.totalWidth,
				height: dim.totalHeight,
				display: 'block'
			}).setPosition({
				relativeTo: element,
				position: 'upperLeft'
			});
			
			this.waiterFx.start({
				0: { opacity:[1] },
				1: { opacity:[this.options.layer.opacity]}
			}).chain(function(){
				this.inTransit = false;
				this.fireEvent('onShow', element);
				this.callChain();
			}.bind(this));
		}.bind(this);
		

		if (this.active && this.active != element) this.stop(start);
		else start();
		
		return this;
	},
	
	stop: function(callback){
		if (this.inTransit) {
			this.chain(this.stop.bind(this, callback));
			return this;
		}
		if (!this.active) return this;
		this.inTransit = true;
		//fade the waiter out
		this.waiterFx.start({
			0: { opacity:[0]},
			1: { opacity:[0]}
		}).chain(function(){
			this.inTransit = false;
			this.active = null;
			this.waiterDiv.hide();
			this.waiterImg.hide();  
			this.fireEvent('onHide', this.active);
			this.callChain();
			if ($type(callback) == "function") callback.attempt();
		}.bind(this));
		return this;
	}
});

Waiter.implement(new Options, new Events, new Chain);

if (typeof Ajax != "undefined") {
	var Ajax = Ajax.extend({
		options: {
			useWaiter: false,
			waiterOptions: {}
		},
		initialize: function(url, options){
			this.parent(url, options);
			if (this.options.useWaiter && this.options.update) {
				this.waiter = new Waiter(this.options.update, this.options.waiterOptions);
				this.addEvent('onComplete', this.waiter.stop.bind(this.waiter));
				this.addEvent('onFailure', this.waiter.stop.bind(this.waiter));
			}
		},
		request: function(data) {
			if (this.waiter) this.waiter.start().chain(this.parent.bind(this, data));
			else this.parent(data);
			return this;
		}
	});
}

Element.extend({

	getDimensions: function() {
		var w = 0;
		var h = 0;
		try { //safari sometimes crashes here, so catch it
			w = this.getStyle('width').toInt();
			h = this.getStyle('height').toInt();
		}catch(e){}
		if((w == 0 || $type(w) != 'number')||(h == 0 || $type(h) != 'number')){
			var holder = new Element('div').setStyles({
				'position':'absolute',
				'top':'-1000px',
				'left':'-1000px'
			}).injectAfter(this);
			var clone = this.clone().injectInside(holder).show();
			w = clone.offsetWidth;
			h = clone.offsetHeight;
			holder.remove();
		}
		return {width: w, height: h, x: w, y: h};
	},

	setPosition: function(options){
		options = $merge({
			relativeTo: document.body,
			position: 'center',
			offset: {x:0,y:0},
			smoothMove: false,
			effectOptions: {},
			returnPos: false
		}, options);
		this.setStyle('position', 'absolute');
		var rel = $(options.relativeTo);
		var top = (rel == document.body)?window.getScrollTop():rel.getTop();
		if (top < 0) top = 0;
		var left = (rel == document.body)?window.getScrollLeft():rel.getLeft();
		if (left < 0) left = 0;
		var dim = this.getDimensions();
		var pos = {};
		var prefY = options.offset.y.toInt();
		var prefX = options.offset.x.toInt();
		switch(options.position) {
			case 'upperLeft':
				pos = {
					'top':(top + prefY) + 'px',
					'left':(left + prefX) + 'px'
				};
				break;
			case 'upperRight':
				pos = {
					'top':(top + prefY) + 'px',
					'left':(left + prefX + rel.offsetWidth) + 'px'
				};
				break;
			case 'bottomLeft':
				pos = {
					'top':(top + prefY + rel.offsetHeight) + 'px',
					'left':(left + prefX) + 'px'
				};
				break;
			case 'bottomRight':
				pos = {
					'top':(top + prefY + rel.offsetHeight) + 'px',
					'left':(left + prefX + rel.offsetWidth) + 'px'
				};
				break;
			default: //center
				var finalTop = top + (((rel == document.body)?window.getHeight():rel.offsetHeight)/2) - (dim.height/2) + prefY;
				var finalLeft = left + (((rel == document.body)?window.getWidth():rel.offsetWidth)/2) - (dim.width/2) + prefX;
				pos = {
					'top': ((finalTop >= 0)?finalTop:0) + 'px',
					'left': ((finalLeft >= 0)?finalLeft:0) + 'px'
				};
				break;
		}
		if(options.returnPos) return pos;
		if(options.smoothMove && this.effects) this.effects(options.effectOptions).start(pos);
		else this.setStyles(pos);
		return this;
	},  

	getComputedSize: function(options){
		options = $merge({
			styles: ['padding','border'],
			plains: {height: ['top','bottom'], width: ['left','right']},
			mode: 'both'
		}, options);
		var size = {width: 0,height: 0};
		switch (options.mode){
			case 'vertical':
				delete size.width;
				delete options.plains.width;
				break;
			case 'horizontal':
				delete size.height;
				delete options.plains.height;
				break;
		}
		var getStyles = [];
		//this function might be useful in other places; perhaps it should be outside this function?
		$each(options.plains, function(plain, key){
			plain.each(function(edge){
				options.styles.each(function(style){
					getStyles.push((style=="border")?style+'-'+edge+'-'+'width':style+'-'+edge);
				});
			});
		});
		var styles = this.getStyles.apply(this, getStyles);
		var subtracted = [];
		$each(options.plains, function(plain, key){ //keys: width, height, plains: ['left','right'], ['top','bottom']
			size['total'+key.capitalize()] = 0;
			size['computed'+key.capitalize()] = 0;
			plain.each(function(edge){ //top, left, right, bottom
				size['computed'+edge.capitalize()] = 0;
				getStyles.each(function(style,i){ //padding, border, etc.
					//'padding-left'.test('left') size['totalWidth'] = size['width']+[padding-left]
					if(style.test(edge)) {
						styles[style] = styles[style].toInt(); //styles['padding-left'] = 5;
						if(isNaN(styles[style]))styles[style]=0;
						size['total'+key.capitalize()] = size['total'+key.capitalize()]+styles[style];
						size['computed'+edge.capitalize()] = size['computed'+edge.capitalize()]+styles[style];
					}
					//if width != width (so, padding-left, for instance), then subtract that from the total
					if(style.test(edge) && key!=style && 
						(style.test('border') || style.test('padding')) && !subtracted.test(style)) {
						subtracted.push(style);
						size['computed'+key.capitalize()] = size['computed'+key.capitalize()]-styles[style];
					}
				});
			});
		});
		if($chk(size.width)) {
			size.width = size.width+this.offsetWidth+size.computedWidth;
			size.totalWidth = size.width + size.totalWidth;
			delete size.computedWidth;
		}
		if($chk(size.height)) {
			size.height = size.height+this.offsetHeight+size.computedHeight;
			size.totalHeight = size.height + size.totalHeight;
			delete size.computedHeight;
		}
		return $merge(styles, size);
	},
	
	visible: function() {
		return this.getStyle('display') != 'none';
	},

	hide: function() {
		this.originalDisplay = this.getStyle('display'); 
		this.setStyle('display','none');
		return this;
	},

	show: function(display) {
		this.setStyle('display',(display || this.originalDisplay || 'block'));
		return this;
	},

	smoothHide: function(effectOptions){
		var styles = this.getStyles('padding-top', 'padding-bottom', 'margin-top', 
			'margin-bottom', 'border-top-width', 'border-bottom-width');
		styles.height = this.offsetHeight-styles['padding-top'].toInt()-styles['padding-bottom'].toInt()-
			styles['border-top-width'].toInt()-styles['border-bottom-width'].toInt()+"px";
		var zero = {height: '0px', opacity: 0};
		$each(styles, function(style, index, name){ zero[name] = 0; });
		this.effects(effectOptions||{}).start(zero).chain(function(){
			this.setStyles(styles).setStyle('display','none');
		}.bind(this));  
	},
	
	smoothShow: function(effectOptions, heightOverride){
		if(this.getStyle('display') == "none" || 
			 this.getStyle('visiblity') == "hidden" || 
			 this.getStyle('opacity')==0){
			//toggle display, but hide it
			this.setStyles({ 'display':'block', 'opacity':0 });
			var h = heightOverride || this.offsetHeight;
			var styles = Object.extend({opacity: 1},
				this.getStyles('padding-top', 'padding-bottom', 'margin-top', 
					'margin-bottom', 'border-top-width', 'border-bottom-width'));
			styles.height = h-styles['padding-top'].toInt()-styles['padding-bottom'].toInt()-
				styles['border-top-width'].toInt()-styles['border-bottom-width'].toInt()+"px";
			var zero = { height: '0px', opacity: 0 };
			$each(styles, function(style, index, name){ zero[name] = 0; });
			this.setStyles(zero).effects(effectOptions||{}).start(styles);
		}
	}
});