function getViewportDimensions() {
	var viewportwidth, viewportheight;
	// the more standards compliant browsers (mozilla/netscape/opera/IE7) use window.innerWidth and window.innerHeight
	if (typeof window.innerWidth != 'undefined') {
		viewportwidth = window.innerWidth;
		viewportheight = window.innerHeight;
	} else if (typeof document.documentElement != 'undefined' && typeof document.documentElement.clientWidth != 'undefined' && document.documentElement.clientWidth != 0) {  // IE6 in standards compliant mode (i.e. with a valid doctype as the first line in the document)
		viewportwidth = document.documentElement.clientWidth;
		viewportheight = document.documentElement.clientHeight;
	} else {  // older versions of IE
		viewportwidth = document.body.clientWidth;
		viewportheight = document.body.clientHeight;
	}
	return [viewportwidth, viewportheight];
}

function getPageDimensions() {
	// the more standards compliant browsers (mozilla/netscape/opera/IE7) use window.innerWidth and window.innerHeight
	var dB = document.body, pageW = 0, pageH = 0;
	if (dB.scrollHeight > dB.offsetHeight) {
		pageW = dB.scrollWidth;
		pageH = dB.scrollHeight;
	} else {
		pageW = dB.offsetWidth;
		pageH = dB.offsetHeight;
	}
	return [pageW, pageH];
}

var allModals = new function() {
	var mList = [],
	openMap = [];
	this.add = function( modalObj ) {
		modalId = mList.length;
		mList.push(modalObj);
		return modalId;
	};
	
	this.remove = function( modalId ) {
		mList[modalId] = null;
	};
	
	this.openSet = function() {
		var workingSet = [];
		$.each(mList, function( i, obj ) {
			if (!!obj && obj.isOpen()) {
				workingSet.push(obj);
			}
		});
		return workingSet;
	};
	
	this.wipe = function() {
		$.each(mList, function( i, obj ) {
			if (!!obj && obj.isOpen() && !obj.isMulti()) {
				obj.close();
			}
		});
	};
};

if ($.browser.msie && ($.browser.version < 7)) {
	$(document).ready(function() {
		$(document.body).append('<style type="text/css">select.modalSelectHide{visibility: hidden !important;}</style>');
	});
}

function Modal( contructorOpts ) {
	function DUMMYopts() {}
	DUMMYopts.prototype = {
		clean: true,  // Modify DOM on open/close
		//multi: false,  // Allow this modal to exist with other modals  /* NOT IMPLEMENTED */
		//title: '',  // The modal's title
		html: '',  // Content to use in the modal
		useAjax: false,  // Uses an ajax request to get content
		url: '',  // URL for the ajax request
		ajaxData: {},  // data for the ajax request
		id: 'dummyModal',
		height: 0,
		width: 0,
		overlay: false,
		shadow: true,
		shadowPad: 16,
		closeSelector: '',
		openCb: function() {},  // Callback to run after the modal opens
		closeCb: function() {}  // Callback to run after the modal closes
	};
	
	function AJAXopts() {}
	AJAXopts.prototype = {
		cache: true,
		dataType: 'html',
		success: function( htmlStr ) {
			isAjaxDone = true;
			setConfig({html: htmlStr});
			if (shouldBeOpen) {
				openModal();
			}
		}
	};
	
	var opts = new DUMMYopts(),
	modalId = -1,
	modalInstance = this,
	modalDOM = $('#' + opts.id),
	modalDOMwrap = $('#' + opts.id + 'Outer'),
	modalDOMoverlay = $('#' + opts.id + 'Overlay'),
	shadowMatrix = [],           // shadowMatrix => 0 1 2 
	isOpen = false,              //                 7   3
	isTransforming = false,      //                 6 5 4
	isAjaxDone = false,
	shouldBeOpen = false,
	OPENanimationTime = 500,
	CLOSEanimationTime = 500,
	FASTanimationTime = 400,
	EASINGeq = 'swing',
	modalDOMoverlayStyles = {
		left: 0,
		top: 0,
		position: 'absolute',
		zIndex: 9997
	},
	modalDOMwrapStyles = {
		position: 'absolute',
		zIndex: 9998
	},
	modalDOMstyles = {
		position: 'absolute',
		zIndex: 9999
	};
	
	function setConfig( configOpts ) {
		if (modalId === -1) {
			modalId = allModals.add(modalInstance);
		}
		if (!!configOpts) {
			var needsRebuild = false;
			$.each(opts, function( key ){
				if (typeof configOpts[key] != 'undefined') {
					switch (key) {
						case 'ajaxData':
						case 'url':
							isAjaxDone = false;
							opts[key] = configOpts[key];
							break;
						case 'id':
						case 'html':
							needsRebuild = true;
							opts[key] = configOpts[key];
							break;
						default:
							opts[key] = configOpts[key];
					}
				}
			});
			if (!!opts.useAjax && !isAjaxDone) {
				var ajaxOpts = new AJAXopts()
				ajaxOpts.data = opts.ajaxData;
				ajaxOpts.url = opts.url;
				$.ajax(ajaxOpts);
			}
			if (needsRebuild) {
				buildDOM();
			}
		}
	}
	
	function buildDOM() {
		destroyDOM();
		if (isTransforming) {
			modalDOM.append(opts.html);
		} else {
			modalDOM = $('<div id="' + opts.id + '" class="modal"></div>');
			modalDOMwrap = $('<div id="' + opts.id + 'Outer" class="modalOuter"></div>');
			modalDOM.appendTo(modalDOMwrap);
			modalDOMoverlay = $('<div id="' + opts.id + 'Overlay" class="modalOverlay"></div>');
			modalDOMoverlay[0].modalRef = modalDOMwrap[0].modalRef = modalDOM[0].modalRef = modalInstance;  // Add a reference to the Modal obj from the DOM
			modalDOMoverlay.hide();
			modalDOMoverlay.css(modalDOMoverlayStyles);
			modalDOMoverlay.appendTo('body');
			modalDOMoverlay.hide();  // Safari 3.x Fix... (can't hide when not IN dom || applying css or adding to dom resets the hide)
			modalDOMwrap.hide();
			modalDOMwrap.css(modalDOMwrapStyles);
			modalDOM.css(modalDOMstyles);
			modalDOMwrap.appendTo('body');
			modalDOMwrap.hide();  // Safari 3.x Fix... (can't hide when not IN dom || applying css or adding to dom resets the hide)
			modalDOM.append(opts.html);  // Moved so that ajax inserted scripts run after objects exist in dom (after modalDOMwrap appends to body)
			if (opts.shadow) {
				addShadow();
			}
			addCloseListeners();
		}
	}
	
	function destroyDOM() {
		if (!!modalDOMwrap.length && !!modalDOM.length) {
			if (isTransforming) {
				modalDOM.empty();
			} else {
				removeShadow();
				modalDOMwrap.remove();
				modalDOMoverlay.remove();
			}
		}
	}
	
	function centerModal( useAnimation ) {
		if (isOpen) {
			var viewportSize = getViewportDimensions(),
			pageSize = getPageDimensions(),
			hasShadow = !!shadowMatrix.length,
			frameHeight = opts.height,
			frameWidth = opts.width,
			frameTop = Math.floor(viewportSize[1] / 2) + $(window).scrollTop(),
			frameLeft = Math.floor(viewportSize[0] / 2);
			
			if (hasShadow) {
				frameHeight += opts.shadowPad * 2;
				frameWidth += opts.shadowPad * 2;
			}
			
			frameTop -= Math.floor(frameHeight / 2);
			frameLeft -= Math.floor(frameWidth / 2);
			
			if (frameTop < 0) frameTop = 0;
			if (frameLeft < 0) frameLeft = 0;
			
			var modalDOManimationStyles = {
				top: opts.shadowPad + 'px',
				left: opts.shadowPad + 'px',
				height: opts.height + 'px',
				width: opts.width + 'px'
			},
			modalDOMwrapAnimationStyles = {
				top: frameTop + 'px',
				left: frameLeft + 'px',
				height: frameHeight + 'px',
				width: frameWidth + 'px'
			},
			modalDOMoverlayAnimationStyles = {
				height: pageSize[1] + 'px',
				width: pageSize[0] + 'px'
			};
			
			$.extend(modalDOMstyles, modalDOManimationStyles);
			$.extend(modalDOMwrapStyles, modalDOMwrapAnimationStyles);
			$.extend(modalDOMoverlayStyles, modalDOMoverlayAnimationStyles);
			
			if (useAnimation) {
				modalDOM.stop();
				modalDOM.animate(modalDOManimationStyles, FASTanimationTime, EASINGeq, function() {
					modalDOM.css(modalDOMstyles);
				});
				modalDOMwrap.stop();
				modalDOMwrap.animate(modalDOMwrapAnimationStyles, FASTanimationTime, EASINGeq, function() {
					modalDOMwrap.css(modalDOMwrapStyles);
				});
				animateShadows();
			} else {
				modalDOM.css(modalDOMstyles);
				modalDOMwrap.css(modalDOMwrapStyles);
			}
			if (opts.overlay) {
				modalDOMoverlay.css(modalDOMoverlayStyles);
			}
		}
	}
	
	function styleShadow( shadowElement, shadowIndex ) {  // Theory says this should be done via CSS :(
		var shadowCSS = {
			position: 'absolute',
			overflow: 'hidden',
			zIndex: 9998
		};
		switch (shadowIndex) {
			case 0:
			case 1:
			case 2:
				shadowCSS.top = 0;
				shadowCSS.height = opts.shadowPad + 'px';
				break;
			case 4:
			case 5:
			case 6:
				shadowCSS.bottom = 0;
				shadowCSS.height = opts.shadowPad + 'px';
				break;
			case 3:
			case 7:
				shadowCSS.top = opts.shadowPad + 'px';
				shadowCSS.height = opts.height + 'px';
				
		}
		switch (shadowIndex) {
			case 0:
			case 6:
			case 7:
				shadowCSS.left = 0;
				shadowCSS.width = opts.shadowPad + 'px';
				break;
			case 2:
			case 3:
			case 4:
				shadowCSS.right = 0;
				shadowCSS.width = opts.shadowPad + 'px';
				break;
			case 1:
			case 5:
				shadowCSS.left = opts.shadowPad + 'px';
				shadowCSS.width = opts.width + 'px';
				
		}
		if ($.browser.msie/* && ($.browser.version < 7)*/) {  // Do for all versions of IE to keep the fades from turning pngs black
			var bgIm = shadowElement.css('backgroundImage').replace(/^url\(\"([^\"]*)\"\)$/, '$1');
			shadowElement.css({backgroundImage: 'none'});
			shadowCSS.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true,sizingMethod=scale,src="' + bgIm + '")';
		}
		return shadowElement.css(shadowCSS);
	}
	
	function animateShadows() {
		if (!!shadowMatrix.length) {
			var vEdgeShadows = [$(shadowMatrix[3]), $(shadowMatrix[7])],
			hEdgeShadows = [$(shadowMatrix[1]), $(shadowMatrix[5])];
			$.each(vEdgeShadows, function( i, obj ) {
				obj.stop();
				obj.animate({height: opts.height + 'px'}, FASTanimationTime, EASINGeq);
			});
			$.each(hEdgeShadows, function( i, obj ) {
				obj.stop();
				obj.animate({width: opts.width + 'px'}, FASTanimationTime, EASINGeq);
			});
		}
	}
	
	function addShadow() {
		if (!shadowMatrix.length) {
			for (var i = 0; i < 8; i++) {
				shadowMatrix.push(styleShadow($('<div class="modalShadow' + i + '"></div>').appendTo(modalDOMwrap), i));
			}
		}
	}
	
	function removeShadow() {
		while(!!shadowMatrix.length) {
			shadowMatrix.pop().remove();
		}
	}
	
	function addCloseListeners() {
		function closeHandler( evt ) {
			evt.preventDefault();
			setTimeout(function() {
				closeModal();
			}, 0);
		}
		
		modalDOMoverlay.unbind('click.modalClose');
		modalDOMoverlay.bind('click.modalClose', closeHandler);
		
		if (!!opts.closeSelector) {
			var closeEls = $(opts.closeSelector);
			if (!!closeEls.length) {
				closeEls.unbind('click.modalClose');
				closeEls.bind('click.modalClose', closeHandler);
			}
		}
	}
	
	function showModal() {
		centerModal();
		modalDOMwrap.stop();
		modalDOMwrap.hide();  // Safari fix... animation skips otherwise ???
		modalDOMwrap.animate({opacity: 'show'}, OPENanimationTime, EASINGeq, opts.openCb);
		
		if (opts.overlay) {
			modalDOMoverlay.stop();
			modalDOMoverlay.hide();  // Safari fix... animation skips otherwise ???
			modalDOMoverlay.animate({opacity: 'show'}, OPENanimationTime, EASINGeq, opts.openCb);
		}
	}
	
	function hideModal() {
		modalDOMwrap.stop();
		modalDOMwrap.animate({opacity: 'hide'}, CLOSEanimationTime, EASINGeq, opts.closeCb);

		if (opts.overlay) {
			modalDOMoverlay.stop();
			modalDOMoverlay.animate({opacity: 'hide'}, CLOSEanimationTime, EASINGeq, opts.closeCb);
		}
	}
	
	function openModal() {
		shouldBeOpen = true;
		if (!isOpen && (!opts.useAjax || isAjaxDone)) {
			if (!!allModals.openSet().length) {
				allModals.wipe();
			}
			isOpen = true;
			if (opts.clean) {
				buildDOM();
			}
			ie6modalSelectFix('hide', modalDOM);
			showModal();
		}
	}
	
	function closeModal() {
		shouldBeOpen = false;
		if (isOpen) {
			isOpen = false;
			hideModal();
			if (opts.clean) {
				modalDOMwrap.queue(function() {
					destroyDOM();  // NOTE: event handlers will be lost... this should be worked out later
					if (!allModals.openSet().length) {
						ie6modalSelectFix('show');  // Should be fixed to only show next open modal if multi is considered
					}
					modalDOMwrap.dequeue();
				});
			}
		}
	}
	
	var ie6modalSelectFix = (function() {
		if ($.browser.msie && ($.browser.version < 7)) {
			return function( toggle, scope ) {
				var bod = $(document.body);
				if (toggle != 'hide') {
					bod.find('select').removeClass('modalSelectHide');
				} else {
					if (!scope) {
						scope = bod;
					} else {
						bod.find('select').addClass('modalSelectHide');
					}
					scope.find('select').removeClass('modalSelectHide');
				}
			};
		} else {
			return function(){};
		}
	})();
		
	modalInstance.isMulti = function() {
		return !!opts.multi;
	};
	
	modalInstance.isOpen = function() {
		return !!isOpen;
	};
	
	modalInstance.transform = function( transformOpts ) {
		modalInstance.retransform = (function() {
			var instanceOpts = opts;
			return function() {
				modalInstance.transform(instanceOpts);
			};
		})();
		
		isTransforming = true;
		modalDOMwrap.addClass('modalTransforming');
		opts = new DUMMYopts();
		setConfig(transformOpts);
		centerModal(true);
		modalDOMwrap.queue(function() {
			modalDOMwrap.removeClass('modalTransforming');
			isTransforming = false;
			transformOpts.openCb();
			modalDOMwrap.dequeue();
		});
	};
	
	modalInstance.retransform = function() {};
	
	modalInstance.open = function( htmlContent ) {
		if (typeof htmlContent == 'string') {
			setConfig({html: htmlContent});
		}
		openModal();
	};
	
	modalInstance.close = function() {
		closeModal();
	};
	
	modalInstance.getModalDOM = function() {
		return modalDOM;
	};
	
	modalInstance.getModalDOMwrap = function() {
		return modalDOMwrap;
	};
	
	modalInstance.init = function( initOpts ) {
		setConfig(initOpts);
		
		$(window).resize(function() {
			centerModal(true);
		});
	};
	modalInstance.init(contructorOpts);
}
