sense.ui.widget.Manager.register({ constructor:'Overlay', selector:'a[rel^=overlay]' });
sense.ui.widget.Overlay = Class.create(sense.ui.widget.Base, {

	// Properties
	defaults: { defaultWidth:500, viewportPadding:20, errorMessage:'Sorry, an unexpected error occurred', imageReplaceHeaders:false, backgroundClicksCloseOverlay:true, useComplexHtml:true, animate:true },
	imgTemplate: new Template('<div class="uiOverlayImage" style="width:#{width}px; height:#{height}px;"><img src="#{src}" /></div>'),
	loadingHtml: '<div class="uiOverlayLoading"></div>',
	minimalHtml: '<div class="uiOverlayWrapper"><div class="uiOverlayHead"><a href="#" class="uiOverlayClose"><span>Close</span></a><h2></h2><div class="clear"></div></div><div class="uiOverlayBody"></div></div>',
	complexHtml: '\
		<div class="bTop"><div class="bTopL"></div><div class="bTopR"></div></div>\
		<div class="bMid"><div class="bMidInner">\
			<div class="uiOverlayHead"><a href="#" class="uiOverlayClose"><span>Close</span></a><h2></h2><div class="clear"></div></div>\
			<div class="uiOverlayBody"></div>\
		</div></div>\
		<div class="bBtm"><div class="bBtmL"></div><div class="bBtmR"></div></div>\
	',

	// Methods
	initialize: function($super, element, options) {
		// Call Parent Constructor
		$super(element, options, this.defaults);

		if (Prototype.Browser.IE6) {
			this.options.useComplexHtml = false;
		}
		if (Prototype.Browser.IE6) {
			this.options.displayOverlayWithPositionOnScroll = true;
		} else {
			this.options.displayOverlayWithPositionFixed = true;
		}

		this.overlayHtml = (this.options.useComplexHtml) ? this.complexHtml : this.minimalHtml;

		if (this.element) {
			switch (this.getType(this.element)) {
				case 'function': this.withFunc(this.element); break;
				case 'link':	 this.withLink(this.element, this.options); break;
				default:		 this.withHTML(this.element, this.options); break;
			}
		}
	},

	getType: function(v) {
		// Type detection extracted from jQuery
		if (v === undefined) {
			var type = 'undefined';
		} else if (typeof v == "string") {
			var type = 'string';
		} else if (v.nodeType) {
			var type = (v.tagName=='A') ? 'link' : 'element';
		} else if (!!v && typeof v != "string" && !v.nodeName && v.constructor != Array && /function/i.test( v + "" )) {
			var type = 'function';
		}
		return type;
	},

	withFunc: function(func) {
		this.loading();
		func.bind(this)();
	},

	withHTML: function(html, opts) {
		this.reveal(html, opts);
	},

	withLink: function(link, opts) {
		// Test if the link points to an image
		var imgTypes = ['png','jpg','jpeg','gif'];
		var imgRegEx = new RegExp('\.' + imgTypes.join('|') + '$', 'i');
		// Test if the link points to an element ID
		var divRegEx = /#/;

		this.imgHandler = this.image;
		this.urlHandler = this.load;

		// Decide which is the appropriate click handler
		if (divRegEx.test(link.href)) {
			var clickHandler = this.divHandler.bind(this);
		} else if (imgRegEx.test(link.href)) {
			var clickHandler = this.imgHandler.bind(this);
		} else {
			var clickHandler = this.urlHandler.bind(this);
		}

		// Extract any key-value option pairs from the rel="..." value
		if (link.rel != 'overlay') {
			var opts = opts || {};
			var optPairs = link.rel.split('|');
			optPairs.each(function(pair){
				var pair = pair.split('=');
				opts[pair[0]] = pair[1];
			});
		}

		// Attach the click handler to the link
		link.observe('click', function(e){
			e.stop();
			clickHandler(link.href, opts);
		}.bind(this));
	},

	divHandler: function(url, opts) {
		var page = window.location.href.split('#')[0];
		var id = url.replace(page+'#','').replace('#','');
		var node = $(id);
		this.reveal(node, opts);
	},

	image: function(url, opts) {
		this.loading();
		var img = new Image();
		img.onload = function(){
			if (!this.inited) return; // Overlay was closed
			this.reveal(this.imgTemplate.evaluate(img), opts);
		}.bind(this);
		img.src = url;
	},

	load: function(url, opts) {
		var match = url.match(/:\/\/(.*?)(?:[:\/]|$)/)
		var local = !(match && document.domain != match[1]);
		if (local) {
			this.loading();
			new Ajax.Request(url, {
				method: 'get',
				onSuccess: function(transport){
					if (!this.inited) return; // Overlay was closed
					var data = transport.responseJSON;
					if (data && data.redirect) {
						window.location.href = data.redirect;
					} else {
						this.reveal(transport.responseText, opts);
					}
				}.bind(this),
				onFailure: function(){
					if (!this.inited) return; // Overlay was closed
					this.reveal(this.options.errorMessage, opts);
				}.bind(this)
			});
		} else {
			this.iframe(url, opts);
		}
	},

	iframe: function(url, opts, callback) {
		var iframe = new Element('iframe', { 'src':url, 'scroll':'no', 'frameborder':'no' }).addClassName('uiOverlayIframe');

		if (callback) {
			iframe.observe('load', callback);
		}
		
		this.reveal(iframe, Object.extend({
			'displayContentFlush':true,
			'displayContentFullScreen':true
		}, opts));
	},

	loading: function() {
		this.reveal(this.loadingHtml, { 'isLoading':true });
	},

	reveal: function(content, opts) {
		if (!content) return;

		// Temporary options exist on a per-overlay basis. Every time a new 
		// "reveal" is performed, the options are cleared or over-written.
		this.temporaryOpts = (typeof opts == 'undefined') ? {} : opts;

		this.create();
		this.display();

		// If the content was a node in the document, we need to remember 
		// where it came from so we can put it back again when we're done
		if (typeof content != "string") {
			this.contentWasElement = true;
			this.contentWasAfter = content.previous();
			if (!this.contentWasAfter) {
				// In IE if this is a dynamically generated element and not
				// currently part of the document, it will have a parent, but 
				// its nodeName will be '#document-fragment'. If this is the 
				// case, just ignore it.
				var parent = content.up();
				if (parent && parent.nodeName != '#document-fragment') {
					this.contentWasInside = parent;
				}
			}
			if (!content.visible()) content.show();
		} else {
			this.contentWasElement = false;
		}

		// Clear the content area
		this.contentArea.update();

		// Insert the content (preserving any radio values in IE)
		var values = this.getRadioValues(content);
		this.contentArea.insert(content);
		this.setRadioValues(content, values);

		var headingText = this.temporaryOpts.headingText || this.extractHeading();
		this.setHeadingText(headingText);

		// We probably don't need to run a lot of intensive page 
		// initialisers if we're just showing the loading indicator 
		if (!this.temporaryOpts.isLoading) {
			document.fire('dom:updated');
		}

		this.toggleUnderlyingFlash(false);
		this.resizeAndPosition();
	},
	
	create: function() {
 		this.hasCurrentOverlay = false;

		if (this.inited) {
 			this.hasCurrentOverlay = true;
		} else {
			if (window.currentOverlay) {
	 			this.hasCurrentOverlay = true;
				window.currentOverlay.destroy();
			}

			this.inited = true;
			this.container = new Element('div').addClassName('uiOverlay').hide();
			this.background = new Element('div').addClassName('uiOverlayBackground').hide();
			this.iframeShim = new Element('iframe', {'src':"javascript:'<html></html>';", 'scroll':'no', 'frameborder':'no' }).addClassName('uiOverlayShim').hide();
			if (!this.container || !this.background || !this.iframeShim) return;

			$(document.body).insert(this.container);
			$(document.body).insert(this.background);
			$(document.body).insert(this.iframeShim);

			// Attach event handlers for closing the overlay when the close link or the
			// background is clicked. Also reposition the overlay when the window is resized.
			this.closeHandler = this.close.bindAsEventListener(this);
			this.resizeAndPositionHandler = this.resizeAndPosition.bindAsEventListener(this);
			this.positionHandler = this.position.bindAsEventListener(this);

			Event.observe(window, 'resize', this.resizeAndPositionHandler);

			if (this.options.displayOverlayWithPositionFixed) {
				this.container.setStyle({ position:'fixed' });
				this.background.setStyle({ position:'fixed' });
				this.iframeShim.setStyle({ position:'fixed' });
			}
			if (this.options.displayOverlayWithPositionOnScroll) {
				Event.observe(window, 'scroll', this.positionHandler);
			}
			if (this.options.backgroundClicksCloseOverlay) {
				this.background.observe('click', this.closeHandler);
				this.background.setStyle({ cursor:'pointer' });
			}

			window.currentOverlay = this;
 		}

		this.container[this.temporaryOpts.isLoading ? 'addClassName':'removeClassName']('isLoading');
		this.container.update(this.overlayHtml);

		this.contentArea = this.container.down('.uiOverlayBody');
		this.headingArea = this.container.down('.uiOverlayHead');
		this.headingText = this.headingArea.down('h2');
		this.closeLink = this.headingArea.down('a.uiOverlayClose');
		this.closeLink.observe('click', this.closeHandler);

		if (this.temporaryOpts.displayContentFlush) {
			this.contentArea.setStyle({ 'padding':0 });
		}
	},
	
	destroy: function() {
		if (!this.inited) return;

		// Remove event handlers
		if (this.closeLink) this.closeLink.stopObserving('click', this.closeHandler);
		Event.stopObserving(window, 'resize', this.resizeAndPositionHandler);

		if (this.options.backgroundClicksCloseOverlay) {
			this.background.stopObserving('click', this.closeHandler);
		}
		if (this.options.displayOverlayWithPositionOnScroll) {
			Event.stopObserving(window, 'scroll', this.positionHandler);
		}

		// If we took any content from the current document, re-insert it
		this.reinstateContent();

		// Remove nodes from the DOM
		this.container.remove();
		this.background.remove();
		this.iframeShim.remove();

		// Nullify all references
		this.inited = null;
		this.container = null;
		this.background = null;
		this.iframeShim = null;

		if (window.currentOverlay) window.currentOverlay = null;
	},

	display: function() {
		// Show all the necessary component elements
		this.container.show();
		this.background.show();
		this.iframeShim.show();

		if (this.temporaryOpts.isLoading) {
			this.headingArea.hide();
		}

		// Do an initial positioning, even before there's any content
		// just so there's no visible jump from top-left to centered.
		// Can't position while the elements are hidden unfortunately.
		this.resizeAndPosition();

		// Do an animated transition, if appropriate
		if (this.options.animate && !this.hasCurrentOverlay) this.animateIn();
	},

	close: function(e) {
		// if (e && this.temporaryOpts.isLoading) return;

		var callback = function() {
			this.toggleUnderlyingFlash(true);
			this.destroy();
		}.bind(this);

		if (this.options.animate) {
			this.animateOut(callback);
		} else {
			callback();
		}

		if (e) e.stop();
	},

	animateIn: function() {
		if (this.options.useComplexHtml && Prototype.Browser.IE) {
			// IE slows to a crawl and visually glitches when animating opacity in
			// combination with transparent PNGs. So just animate in the background 
			// instead and only show the container once we're done.
			var el = this.container;
			el.setStyle({ visibility:'hidden' });
			var fn = function(){ el.setStyle({ visibility:'visible' }); }
			this.background.tween().fadeIn({ seconds:0.3, onEnd:fn });
		} else {
			this.container.tween().fadeIn({ seconds:0.3 });
			this.background.tween().fadeIn({ seconds:0.3 });
		}
	},
	
	animateOut: function(callback) {
		if (this.options.useComplexHtml && Prototype.Browser.IE) {
			this.container.setStyle({ visibility:'hidden' });
			this.background.tween().fadeOut({ seconds:0.3, onEnd:callback });
		} else {
			this.container.tween().fadeOut({ seconds:0.3 });
			this.background.tween().fadeOut({ seconds:0.3, onEnd:callback });
		}
	},

	resizeAndPosition: function(e) {
		this.resize();
		this.position();
		// This is handled seperately because the dimensions of the 
		// document can change when the overlay is resized & positioned.
		this.resizeBackground();
		this.resizeIframeShim();
	},

	resize: function() {
		// These are done seperately because the height of the content can change 
		// once the width of the overlay is amended (since text is reflowed etc.)
		this.resizeWidth();
		this.resizeHeight();
	},

	resizeWidth: function() {
		var contentElement = this.getContentElement();
		var maxW = this.maxContentWidth();

		if (this.temporaryOpts.contentWidth) {
			var w = parseInt(this.temporaryOpts.contentWidth, 10);
		} else if (this.temporaryOpts.displayContentFullScreen) {
			var w = maxW;
		} else if (contentElement) {
			var w = contentElement.getWidth();
		} else {
			var w = this.options.defaultWidth;
		}

		// Adjust for content potentially being too wide to fit on screen
		if (w > maxW) w = maxW;

		if (contentElement && this.temporaryOpts.displayContentFlush && this.temporaryOpts.displayContentFullScreen) {
			contentElement.setStyle({ 'width':this.px(w) });
		}

		this.container.setStyle({ 'width':this.px(w + this.getHorizontalPadding() + this.getHorizontalFurniture()) });
	},

	resizeHeight: function() {
		var contentElement = this.getContentElement();
		var maxH = this.maxContentHeight();

		// Briefly relax any explicitly set height on the 
		// contentArea, so it doesn't impact measurements.
		this.contentArea.setStyle({ 'height':'auto' });

		if (this.temporaryOpts.contentHeight) {
			var h = parseInt(this.temporaryOpts.contentHeight, 10);
		} else if (this.temporaryOpts.displayContentFullScreen) {
			var h = maxH;
		} else if (contentElement) {
			var h = contentElement.getHeight();
		} else {
			// Measure the content height by getting the contentArea to auto-fit around it.
			var h = this.contentArea.getHeight();
			// If the contentArea is empty for whatever reason, IE returns 0 height,
			// and we don't want a negative content height since it'll be invalid.
			if (h > 0) h -= this.getVerticalPadding();
		}

		// Adjust for content potentially being too tall to fit on screen
		if (h > maxH) h = maxH;

		if (contentElement && this.temporaryOpts.displayContentFlush && this.temporaryOpts.displayContentFullScreen) {
			// If this is a full-screen iframe, expand the content element to fill the container.
			// contentElement.setStyle({ 'height':this.px(h), 'width':this.px(w - this.getHorizontalPadding() - this.getHorizontalFurniture()) });
			var w = this.container.getWidth() - this.getHorizontalPadding() - this.getHorizontalFurniture();
			contentElement.setStyle({ 'height':this.px(h), 'width':this.px(w) });
		} else if (h == maxH) {
			// Widen the overlay to allow some space for the overflow scrollbar.
			var w = this.container.getWidth() + 17;
			this.container.setStyle({ 'width':this.px(w) });
		}

		this.contentArea.setStyle({ 'height':h + 'px' }); // this.px(h) });
	},

	resizeIframeShim: function() {
		// var t = this.container.getStyle('top');
		// var l = this.container.getStyle('left');
		// var w = this.container.getWidth();
		// var h = this.container.getHeight();
		// this.iframeShim.setStyle({ 'top':t, 'left':l, 'width':this.px(w), 'height':this.px(h) });
		var wh = this.background.getDimensions();
		this.iframeShim.setStyle({ 'top':this.px(0), 'left':this.px(0), 'width':this.px(wh.width), 'height':this.px(wh.height) });
	},

	resizeBackground: function() {
		this.background.setStyle({ 'height':'100%' });
		this.iframeShim.setStyle({ 'height':'100%' });

		// The correct scroll height for the document can come from any one of
		// number of sources in different circumstances and different browsers.
		var h = Math.max(
			document.body.scrollHeight, $$('html').first().getHeight(),
			document.documentElement.clientHeight || document.body.clientHeight
		);

		this.background.setStyle({ 'height':this.px(h) });
	},

	position: function() {
		var h1 = document.viewport.getHeight();
		var w1 = document.viewport.getWidth();

		var h2 = this.container.getHeight();
		var w2 = this.container.getWidth();

		// Experimental: May trigger bugs in some browsers.
		if (this.options.displayOverlayWithPositionFixed) {
			var scroll = [0,0];
		} else {
			var scroll = document.viewport.getScrollOffsets();
		}

		var x = scroll[0] + (w1/2) - (w2/2);
		var y = scroll[1] + (h1/2) - (h2/2);

		// Prevent the window from sitting right at the top of the screen
		if (y<this.options.viewportPadding) y = this.options.viewportPadding;

		this.container.setStyle({top:this.px(y), left:this.px(x)});
	},

	maxOverlayWidth: function() {
		return document.viewport.getWidth() - (this.options.viewportPadding*2);
	},
	maxOverlayHeight: function() {
		return document.viewport.getHeight() - (this.options.viewportPadding*2);
	},
	maxContentWidth: function() {
		return this.maxOverlayWidth() - this.getHorizontalFurniture() - this.getHorizontalPadding();
	},
	maxContentHeight: function() {
		return this.maxOverlayHeight() - this.getVerticalFurniture() - this.getVerticalPadding();
	},

	getHorizontalFurniture: function() {
		// Difference in width between outermost & innermost element
		var w1 = this.container.getWidth();
		var w2 = this.contentArea.getWidth();
		return w1 - w2;
	},
	getHorizontalPadding: function() {
		// Combined width of padding on innermost element
		var padLft = parseInt(this.contentArea.getStyle('paddingLeft'),10);
		var padRgt = parseInt(this.contentArea.getStyle('paddingRight'),10);
		return padLft + padRgt;
	},

	getVerticalFurniture: function() {
		// Difference in height between outermost & innermost element
		var h1 = this.container.getHeight();
		var h2 = this.contentArea.getHeight();
		return h1 - h2;
	},
	getVerticalPadding: function() {
		// Combined height of padding on innermost element
		var padTop = parseInt(this.contentArea.getStyle('paddingTop'),10);
		var padBtm = parseInt(this.contentArea.getStyle('paddingBottom'),10);
		return padTop + padBtm;
	},

	getContentElement: function() {
		// var elems = this.contentArea.childElements();
		// return (elems.length == 1) ? elems.first() : false;
		var nodes = this.contentArea.childNodes;
		nodes = $A(nodes).select(function(node){ return (node.nodeType != Node.TEXT_NODE); });
		return (nodes.length == 1 && nodes[0].nodeType == Node.ELEMENT_NODE) ? nodes[0] : false;
	},

	px: function(n) {
		// Try and make sure we don't use any non-integer values for CSS
		// pixel measurements since they can cause slight visual oddities
		return Math.round(n) + 'px';
	},

	setHeadingText: function(heading) {
		if (heading && this.options.imageReplaceHeaders) {
			this.headingText.id = this.generateHeadingId(heading);
			this.headingText.update(new Element('span').addClassName('accessibility').update(heading));
		} else {
			this.headingText.update(heading);
		}
	},

	generateHeadingId: function(string) {
		// Convert something like "Hey! What's going on?" into "hHeyWhatsGoingOn"
		// Strip out any non-alphanumeric, non-space characters and split into an array of words
		var words = string.replace(/[^a-z0-9 ]+/gi,'').split(/ +/);
		// Iterate through the list of words and uppercase the first letter in each
		words = words.map(function(w){ return w.slice(0,1).toUpperCase() + w.slice(1,w.length); });
		return 'h' + words.join('');
	},

	extractHeading: function(content) {
		var headingText = '';
		var descendants = this.contentArea.descendants();
		for (var i=0; i<descendants.length; i++) {
			// Find the first header element, get its text content,
			// strip any tags from that text, and hide the element.
			if (descendants[i].tagName.match(/h[1-6]/i)) {
				headingText = descendants[i].innerHTML;
				headingText = headingText.replace(/<[^>]*>/g,'');
				descendants[i].hide();
				break;
			}
		}
		return headingText;
	},

	reinstateContent: function() {
		if (!this.contentWasElement) return;

		var content = this.container.down('.uiOverlayBody').down();
		if (!content) return;

		var values = this.getRadioValues(content);
		if (this.contentWasAfter) {
			this.contentWasAfter.insert({ after:content.hide() });
		} else if (this.contentWasInside) {
			this.contentWasInside.insert(content.hide());
		}
		this.setRadioValues(content, values);
	},

	toggleUnderlyingFlash: function(toggle) {
		// Overlaying multiple flash movies seems to cause ugly flickering in
		// Firefox on a PowerPC Mac. So, for this browser, when displaying flash
		// in an overlay, we temporarily hide any underlying flash until it's closed.
		// Also found an issue in linux where flash movies show through the overlay.

		// if ((Prototype.Browser.Gecko && navigator.platform == 'MacPPC' && this.contentArea.down('object, embed'))
		// ||	(Prototype.Browser.Gecko && navigator.platform == 'Linux i686')) {

		// Displaying flash in the overlay while displaying flash in the page
		// seems to be either buggy or slow in practically all browsers.
		var flashInOverlay = this.contentArea.down('object, embed');
		if (flashInOverlay || navigator.platform == 'Linux i686') {
			$$('object, embed').each(function(flash){
				if (flashInOverlay && flashInOverlay.include(flash)) return;
				flash.setStyle({ visibility: toggle ? 'visible' : 'hidden' });
			});
		}
	},

	// When moving a node around by inserting it into a new position in the DOM
	// IE doesn't properly carry over radio button selections. This is necessary when
	// temporarily moving an optional part of a main form out into overlay.
	// So, we store the radio selections for IE re-instate them after the node is moved.

	getRadioValues: function(content) {
		if (!this.contentWasElement) return;
		var checks = content.select('input[type=radio], input[type=checkbox]');
		var values = {};
		for (var i=0; i<checks.length; i++) {
			if (checks[i].checked) values[checks[i].name] = checks[i].value;
		}
		return values;
	},
	setRadioValues: function(content, values) {
		if (!this.contentWasElement) return;
		var checks = content.select('input[type=radio], input[type=checkbox]');
		for (var i=0; i<checks.length; i++) {
			var key = checks[i].name;
			var val = checks[i].value;
			if (values[key] == val) checks[i].checked = true;
		}
	}

});
