ChangeDetector = new Class({
	initialize: function(object, attribute, callback, period) {
		var getValue = function() {
			if (typeof attribute == 'function') {
				return attribute(object);
			} else {
				return object[attribute];
			}
		};
		var previousValue = getValue();
		this.poller = (function() {
			var currentValue = getValue();
			if (currentValue !== previousValue) {
				callback.apply(this, [object, attribute, previousValue, currentValue]);
				previousValue = currentValue;
			}
		}).bind(this);
		this.timer = null;
		this.start(period);
	},
	
	start: function(period) {
		this.stop();
		this.timer = this.poller.periodical(period || 100);
	},
	
	stop: function() {
		$clear(this.timer);
	}
});

var ModalDialog = new Class({

	Implements: [Options, Events],

	options: {
		topButtons: [],
		bottomButtons: [],
		height: null,
		width: '400px',
		maxWidth: null,
		maxHeight: null,
		
		overlayClassName: 'modal_overlay',
		overlayOpacity: 0.8,
		loadingMessageText: 'Please wait...',
		loadingMessageClassName: 'modal_loading',
		dialogClassName: 'modal_dialog',
		contentClassName: 'modal_content',
		topButtonContainerClassName: 'modal_buttons modal_buttons_top',
		bottomButtonContainerClassName: 'modal_buttons modal_buttons_bottom',
		loaderErrorMessage: '<h2>Error</h2>$MESSAGE$</p><p>Please try again.</p>',
		gutter: 10
	},
	
	initialize: function(loader, options) {
		var self = this;
	
		this.setOptions({
			bottomButtons: [new ModalDialog.Button('OK', $empty, 'button default')]
		});
		this.setOptions(options);
	
		// Keep a record of all open ModalDialogs
		ModalDialog.instances.push(this);

		// The overlay greys-out the page and prevents the user from iteracting with it 
		this.overlay = new Element('div', {
			'class': this.options.overlayClassName
		});
		this.overlay.setOpacity(this.options.overlayOpacity);
		this.overlay.addEvent('click', function() {
			self.overlay.setStyle('backgroundColor', '#fff');
			(function() {
				if (self.options.overlayClassName) {
					self.overlay.setStyle('backgroundColor', '');
				} else {
					self.overlay.setStyle('backgroundColor', '#000');
				}
			}).delay(50);
		});
		
		if((Browser.Engine.trident && Browser.Engine.version < 5)
				|| (Browser.Engine.gecko && Browser.Engine.version < 19)) {
			// Chuck an extra iframe inside the overlay to prevent selects (in IE6: trident v4)
			// or scrollbars (Firefox < 3.0: gecko v19) from sitting on top of the dialog.
			var hack = new Element('iframe', {
				frameborder: '0',
				styles: {
					border: 'none',
					width: '100%',
					height: '100%'
				}
			});
			hack.setOpacity(0.01);
			hack.injectInside(this.overlay);
		}
		
		
		// This message is displayed while the content for the dialog is loading
		this.loadingMessage = new Element('div', {
			'class': this.options.loadingMessageClassName,
			'text': this.options.loadingMessageText
		});
		
		
		// This is the modal dialog box itself
		this.dialog = new Element('div', {
			'class': this.options.dialogClassName
		});


		this.topButtonContainer = new Element('div', {
			'class': this.options.topButtonContainerClassName
		});
		this.topButtonContainer.injectInside(this.dialog);


		// This is the container for the content of the modal dialog box
		this.content = new Element('div', {
			'class': this.options.contentClassName
		});
		this.content.injectInside(this.dialog);


		this.bottomButtonContainer = new Element('div', {
			'class': this.options.bottomButtonContainerClassName
		});
		this.bottomButtonContainer.injectInside(this.dialog);
		

		this.setTopButtons(this.options.topButtons);
		this.setBottomButtons(this.options.bottomButtons);
		
		// Ensure the dialog is initially hidden
		this.dialog.setStyle('visibility', 'hidden');
		
		// Add the elements to the page 
		$(document.body).adopt(this.overlay, this.loadingMessage, this.dialog);
		
		// Configure standard styling
		this.overlay.setStyles({
			left: 0,
			position: 'absolute',
			zIndex: 1000000
		});
		this.loadingMessage.setStyles({
			left: '50%',
			marginLeft: '-5em',
			position: 'absolute',
			textAlign: 'center',
			width: '10em',
			zIndex: 1000001
			});
		this.dialog.setStyles({
			position: 'absolute',
			zIndex: 1000000
		});

		// Position the overlay and loading message
		this.resize();

		// Ensure the elements update approprately when the window is scrolled or resized
		this.boundResize = this.resize.bind(this);
		this.boundPosition = this.position.bind(this);
		window.addEvent('resize', this.boundPosition);
		window.addEvent('resize', this.boundResize);
		window.addEvent('scroll', this.boundResize);
		
		var showDialog = (function() {
			self.loadingMessage.dispose()
			self.dialog.setStyle('visibility', 'visible');
			self.position();
			self.resize();
		});
		
		// Reposition the dialog as the content size changes
		this.contentHeightDetector = new ChangeDetector(this.content, 'offsetHeight', function() {
			self.position();
		});
		
		var loading = {
			// Normal use: call setContent with content (html string or elements)
			setContent: (function(content) {
				if(typeof content == "string") {
					self.content.set('html', content);
				} else {
					if(content)
						self.content.adopt(content);
				}
				showDialog();
			}),

			// Alternative: provide own container and load it manually, indicating when done
			setContentContainer: (function(container) {
				container = $(container)
				container.injectBefore(self.content);
				self.contentHeightDetector.stop();
				self.contentHeightDetector = null;
				self.content.destroy();
				self.content = container;
			}),
			contentLoaded: (function() {
				showDialog();
			}),

			// Or indicate an error
			error: (function(message) {
				self.close();			
				var errorMessage = self.options.loaderErrorMessage.replace("$MESSAGE$", message);
				ModalDialog.alert(errorMessage);
			})
		};

		loader(loading);
		
	},
	
	position: function() {
	
		// Work out the dimensions of the screen, ignoring the overlay and dialog.
		this.overlay.setStyle('display', 'none');
		this.dialog.setStyle('display', 'none');
		var clientHeight = document.documentElement.clientHeight;
		var clientWidth = document.documentElement.clientWidth;
		var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
		var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
		this.overlay.setStyle('display', 'block');
		this.dialog.setStyle('display', 'block');

		if (this.options.width) {
			this.content.setStyle('width', this.options.width);
		}
		if (this.options.height) {
			this.content.setStyle('height', this.options.height);
		}
		if (this.options.maxHeight) {
			if (this.content.getScrollSize().y > this.options.maxHeight) {
				this.content.setStyle('height', this.options.maxHeight);
			}
		}
		if (this.options.maxWidth) {
			if (this.content.getScrollSize().x > this.options.maxWidth) {
				this.content.setStyle('width', this.options.maxWidth);
			}
		}
	
		var top = Math.max(scrollTop + this.options.gutter, scrollTop + clientHeight / 2 - this.dialog.offsetHeight / 2);
		var left = Math.max(scrollLeft + this.options.gutter, scrollLeft + clientWidth / 2 - this.dialog.offsetWidth / 2);
	
		this.dialog.setStyles({
			top: top,
			left: left
		});
	},
	
	resize: function() {
	
		// Work out the dimensions of the screen, ignoring the overlay, but
		// including the dialog itself.
		this.overlay.setStyle('display', 'none');
		var screenHeight = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
		var screenWidth = Math.max(document.documentElement.scrollWidth, document.body.scrollWidth)
		var clientHeight = document.documentElement.clientHeight;
		var clientWidth = document.documentElement.clientWidth;
		var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
		this.overlay.setStyle('display', 'block');

		this.overlay.setStyles({
			height: screenHeight,
			width: screenWidth,
			left: 0,
			top: 0
		});
			
		// Vertically center the loading message
		this.loadingMessage.setStyle('top', scrollTop + clientHeight / 2 - this.loadingMessage.offsetHeight / 2);
	
		// If the dialog is smaller than the viewport, center it
		if (this.dialog.offsetHeight < (clientHeight - 2 * this.options.gutter) && this.dialog.offsetWidth < (clientWidth - 2 * this.options.gutter)) {
			this.position();
		}
	
	},
	
	close: function(arg) {
		this.fireEvent('close', [this, arg]);
		window.removeEvent('resize', this.boundPosition);
		window.removeEvent('resize', this.boundResize);
		window.removeEvent('scroll', this.boundResize);
		if (this.contentHeightDetector) {
			this.contentHeightDetector.stop();
		}
		this.overlay.destroy();
		this.loadingMessage.destroy();
		this.dialog.destroy();
		ModalDialog.instances.erase(this);
	},
	
	setButtons: function(container, buttons) {
		container.empty();
		for (var i = 0; i < buttons.length; i++) {
			var boundClick = function(event, button) {
				if (!button.onClick(this)) {
					button.element.removeEvent('click', boundClick);
					this.close(button);
				}
				event.preventDefault();
			}.bindWithEvent(this, buttons[i]);
			
			buttons[i].element.addEvent('click', boundClick);
			container.appendChild(buttons[i].element);
		}
	},

	setTopButtons: function(buttons) {
		this.setButtons(this.topButtonContainer, buttons);
	},

	setBottomButtons: function(buttons) {
		this.setButtons(this.bottomButtonContainer, buttons);
	}
	
});


// Record the open ModalDialogs
ModalDialog.instances = new Array();


ModalDialog.Button = new Class({
	initialize: function(labelOrElement, onClick, className) {
		if (typeof labelOrElement == "string") {
			this.element = new Element('a', {
				'class': className || 'button',
				'href': '#',
				'text': labelOrElement
			});
		} else {
			this.element = labelOrElement;
		}
		this.onClick = (onClick || $empty).bind(this);
	}
});

// Builtin loaders
ModalDialog.ContentLoader = function(content) {
	var loader = function(loading) {
		loading.setContent(content);
	};
	return loader;
};

ModalDialog.IFrameLoader = function(url, method, data, timeout) {
	method = method || 'get'
	data = data || {};
	timeout = timeout || 10000;
	
	var contentIFrame = new Element('iframe', {
		'frameborder': '0',
		'name': ('modal_iframe' + Math.round(Math.random() * 10000)),
		'class': 'modal_iframe',
		'src': null
	});

	var loader = function(loading) {
		loading.setContentContainer(contentIFrame);
		
		var timer;

		var onLoad = function() {
			$clear(timer);
			loading.contentLoaded();
		};
		
		timer = (function () {
			contentIFrame.removeEvent('load', onLoad);
			loading.error("The requested operation timed out because the page took too long to load.");
		}).delay(timeout);
		
		contentIFrame.addEvent('load', onLoad);
		
		if(method == 'post') {
			var form = new Element('form', {
				'target': contentIFrame.name,
				'method': 'post',
				'action': url
			});
			for(var key in data) {
				form.adopt(new Element('input', {
					'type': 'hidden',
					'name': key,
					'value': data[key]
				}));
			}
			form.injectAfter(contentIFrame);
			form.submit();
			form.remove();
		} else {
			contentIFrame.src = url;
		}
	};
	return loader;
};

ModalDialog.AjaxLoader = function(url, method, data, evalScripts, timeout) {
	method = method || 'get';
	data = data || {};
	evalScripts = evalScripts || false;
	timeout = timeout || 10000;

	var loader = function(loading) {
		var timer;
		
		var capturedScripts = null;
		var captureScripts = function(scripts, text) {
			capturedScripts = scripts;
		};
		
		var request = new Request({
			url: url,
			method: method,
			evalScripts: captureScripts,
			onSuccess: function(elements) {
				$clear(timer);
				loading.setContent(elements);
				if (evalScripts && capturedScripts) {
					$exec(capturedScripts);
				} 
			},
			onFailure: function(xhr) {
				$clear(timer);
				loading.error(xhr.status.toString() + ' ' + xhr.statusText.toString().toLowerCase().capitalize());
			}
		});
		
		timer = (function () {
			request.cancel();
			loading.error("The requested operation timed out because the page took too long to load.");
		}).delay(timeout);
		
		request.send(data);
	};
	return loader;
};


// Retrieve the topmost open ModalDialog (or null if there are none open)
ModalDialog.getTopmost = function() {
	return ModalDialog.instances.getLast();
};

// Close the topmost open ModalDialog (if there is one);
ModalDialog.close = function(arg) {
	var topmost = ModalDialog.getTopmost();
	if (topmost) topmost.close(arg);
};

ModalDialog._alert = function(loader, callback, options) {
	return new ModalDialog(loader, $merge({
		bottomButtons: [new ModalDialog.Button('OK', function() { if (callback) { callback(true); } })],
		dialogClassName: 'modal_dialog modal_dialog_alert'
	}, options));
};

ModalDialog.alert = function(msg, callback, options) {
	return ModalDialog._alert(ModalDialog.ContentLoader(msg), callback, options);
};

ModalDialog.alertUrl = function(url, callback, options) {
	return ModalDialog._alert(ModalDialog.AjaxLoader(url), callback, options);
};

ModalDialog.confirm = function(msg, callback, options) {
	new ModalDialog(ModalDialog.ContentLoader(msg), $merge({
		bottomButtons: [
			new ModalDialog.Button('OK', function() { callback(true); }, 'button default'),
			new ModalDialog.Button('Cancel', function() { callback(false); })
		],
		dialogClassName: 'modal_dialog modal_dialog_confirm'
	}, options));
};