dojo.provide("dojox.image.Lightbox");
dojo.experimental("dojox.image.Lightbox");

dojo.require("dojo.window");
dojo.require("dijit.Dialog");
dojo.require("dojox.fx._base");

dojo.declare("dojox.image.Lightbox",
	dijit._Widget, {
	// summary:
	//		A dojo-based Lightbox implementation.
	//
	// description:
	//	An Elegant, keyboard accessible, markup and store capable Lightbox widget to show images
	//	in a modal dialog-esque format. Can show individual images as Modal dialog, or can group
	//	images with multiple entry points, all using a single "master" Dialog for visualization
	//
	//	key controls:
	//		ESC - close
	//		Down Arrow / Rt Arrow / N - Next Image
	//		Up Arrow / Lf Arrow / P - Previous Image
	//
	// example:
	// |	<a href="image1.jpg" dojoType="dojox.image.Lightbox">show lightbox</a>
	//
	// example:
	// |	<a href="image2.jpg" dojoType="dojox.image.Lightbox" group="one">show group lightbox</a>
	// |	<a href="image3.jpg" dojoType="dojox.image.Lightbox" group="one">show group lightbox</a>
	//
	// example:
	// |	not implemented fully yet, though works with basic datastore access. need to manually call
	// |	widget._attachedDialog.addImage(item,"fromStore") for each item in a store result set.
	// |	<div dojoType="dojox.image.Lightbox" group="fromStore" store="storeName"></div>
	//
	// group: String
	//		Grouping images in a page with similar tags will provide a 'slideshow' like grouping of images
	group: "",

	// title: String
	//		A string of text to be shown in the Lightbox beneath the image (empty if using a store)
	title: "",

	// href; String
	//		Link to image to use for this Lightbox node (empty if using a store).
	href: "",

	// duration: Integer
	//		Generic time in MS to adjust the feel of widget. could possibly add various
	//		durations for the various actions (dialog fadein, sizeing, img fadein ...)
	duration: 500,

	// modal: Boolean
	//		If true, this Dialog instance will be truly modal and prevent closing until
	//		explicitly told to by calling hide() or clicking the (x) - Defaults to false
	//		to preserve previous behaviors. (aka: enable click-to-click on the underlay)
	modal: false,

	// _allowPassthru: Boolean
	//		Privately set this to disable/enable natural link of anchor tags
	_allowPassthru: false,
	
	// _attachedDialg: dojox.image._LightboxDialog
	//		The pointer to the global lightbox dialog for this widget
	_attachedDialog: null, // try to share a single underlay per page?

	startup: function(){
		this.inherited(arguments);
		// setup an attachment to the masterDialog (or create the masterDialog)
		var tmp = dijit.byId('dojoxLightboxDialog');
		if(tmp){
			this._attachedDialog = tmp;
		}else{
			// this is the first instance to start, so we make the masterDialog
			this._attachedDialog = new dojox.image.LightboxDialog({ id: "dojoxLightboxDialog" });
			this._attachedDialog.startup();
		}
		if(!this.store){
			// FIXME: full store support lacking, have to manually call this._attachedDialog.addImage(imgage,group) as it stands
			this._addSelf();
			this.connect(this.domNode, "onclick", "_handleClick");
		}

	},

	_addSelf: function(){
		// summary: Add this instance to the master LightBoxDialog
		this._attachedDialog.addImage({
			href: this.href,
			title: this.title
		}, this.group || null);
	},

	_handleClick: function(/* Event */e){
		// summary: Handle the click on the link
		if(!this._allowPassthru){ e.preventDefault(); }
		else{ return; }
		this.show();
	},

	show: function(){
		// summary: Show the Lightbox with this instance as the starting point
		this._attachedDialog.show(this);
	},
	
	hide: function(){
		// summary: Hide the Lightbox currently showing
		this._attachedDialog.hide();
	},

	// FIXME: switch to .attr, deprecate eventually.
	disable: function(){
		// summary: Disables event clobbering and dialog, and follows natural link
		this._allowPassthru = true;
	},

	enable: function(){
		// summary: Enables the dialog (prevents default link)
		this._allowPassthru = false;
	},
	
	onClick: function(){
		// summary:
		//		Stub fired when the image in the lightbox is clicked.
	},
	
	destroy: function(){
		this._attachedDialog.removeImage(this);
		this.inherited(arguments);
	}

});

dojo.declare("dojox.image.LightboxDialog",
	dijit.Dialog, {
	// summary:
	//		The "dialog" shared	 between any Lightbox instances on the page, publically available
	//		for programatic manipulation.
	//
	// description:
	//
	//		A widget that intercepts anchor links (typically around images)
	//		and displays a modal Dialog. this is the actual Dialog, which you can
	//		create and populate manually, though should use simple Lightbox's
	//		unless you need the direct access.
	//
	//		There should only be one of these on a page, so all dojox.image.Lightbox's will us it
	//		(the first instance of a Lightbox to be show()'n will create me If i do not exist)
	//
	//	example:
	//	|	// show a single image from a url
	//	|	var url = "http://dojotoolkit.org/logo.png";
	//	|	var dialog = new dojox.image.LightboxDialog().startup();
	//	|	dialog.show({ href: url, title:"My Remote Image"});
	//
	// title: String
	//		The current title, read from object passed to show()
	title: "",

	// FIXME: implement titleTemplate

	// inGroup: Array
	//		Array of objects. this is populated by from the JSON object _groups, and
	//		should not be populate manually. it is a placeholder for the currently
	//		showing group of images in this master dialog
	inGroup: null,

	// imgUrl: String
	//		The src="" attribute of our imageNode (can be null at statup)
	imgUrl: dijit._Widget.prototype._blankGif,
		
	// errorMessage: String
	//		The text to display when an unreachable image is linked
	errorMessage: "Image not found.",

	// adjust: Boolean
	//		If true, ensure the image always stays within the viewport
	//		more difficult than necessary to disable, but enabled by default
	//		seems sane in most use cases.
	adjust: true,

	// modal: Boolean
	//		If true, this Dialog instance will be truly modal and prevent closing until
	//		explicitly told to by calling hide() or clicking the (x) - Defaults to false
	//		to preserve previous behaviors. (aka: enable click-to-click on the underlay)
	modal: false,

/*=====
	// _groups: Object
	//		an object of arrays, each array (of objects) being a unique 'group'
	_groups: { XnoGroupX: [] },

=====*/

	// errorImg: Url
	//		Path to the image used when a 404 is encountered
	errorImg: dojo.moduleUrl("dojox.image","resources/images/warning.png"),

	templateString: dojo.cache("dojox.image","resources/Lightbox.html"),
	constructor: function(args){
		this._groups = this._groups || (args && args._groups) || { XnoGroupX:[] };
	},

	startup: function(){
		// summary: Add some extra event handlers, and startup our superclass.
		//
		// returns: dijit._Widget
		//		Perhaps the only `dijit._Widget` that returns itself to allow
		//		'chaining' or var referencing with .startup()

		this.inherited(arguments);
		
		this._animConnects = [];
		this.connect(this.nextButtonNode, "onclick", "_nextImage");
		this.connect(this.prevButtonNode, "onclick", "_prevImage");
		this.connect(this.closeButtonNode, "onclick", "hide");
		this._makeAnims();
		this._vp = dojo.window.getBox();
		return this;
	},

	show: function(/* Object */groupData){
		// summary: Show the Master Dialog. Starts the chain of events to show
		//		an image in the dialog, including showing the dialog if it is
		//		not already visible
		//
		// groupData: Object
		//		needs href and title attributes. the values for this image.
		//
		//
		var _t = this; // size
		this._lastGroup = groupData;
		
		// we only need to call dijit.Dialog.show() if we're not already open.
		if(!_t.open){
			_t.inherited(arguments);
			_t._modalconnects.push(
				dojo.connect(dojo.global, "onscroll", this, "_position"),
				dojo.connect(dojo.global, "onresize", this, "_position"),
				dojo.connect(dojo.body(), "onkeypress", this, "_handleKey")
			);
			if(!groupData.modal){
				_t._modalconnects.push(
					dojo.connect(dijit._underlay.domNode, "onclick", this, "onCancel")
				);
			}
		}
		
		if(this._wasStyled){
			// ugly fix for IE being stupid. place the new image relative to the old
			// image to allow for overriden templates to adjust the location of the
			// titlebar. DOM will remain "unchanged" between views.
			var tmpImg = dojo.create("img", null, _t.imgNode, "after");
			dojo.destroy(_t.imgNode);
			_t.imgNode = tmpImg;
			_t._makeAnims();
			_t._wasStyled = false;
		}
		
		dojo.style(_t.imgNode,"opacity","0");
		dojo.style(_t.titleNode,"opacity","0");
		
		var src = groupData.href;
		
		if((groupData.group && groupData !== "XnoGroupX") || _t.inGroup){
			if(!_t.inGroup){
				_t.inGroup = _t._groups[(groupData.group)];
				// determine where we were or are in the show
				dojo.forEach(_t.inGroup, function(g, i){
					if(g.href == groupData.href){
						_t._index = i;
						//return false;
					}
					//return true;
				});
			}
			if(!_t._index){
				_t._index = 0;
				var sr = _t.inGroup[_t._index];
				src = (sr && sr.href) || _t.errorImg;
			}
			// FIXME: implement titleTemplate
			_t.groupCount.innerHTML = " (" + (_t._index + 1) + " of " + Math.max(1, _t.inGroup.length) + ")";
			_t.prevButtonNode.style.visibility = "visible";
			_t.nextButtonNode.style.visibility = "visible";
		}else{
			// single images don't have buttons, or counters:
			_t.groupCount.innerHTML = "";
			_t.prevButtonNode.style.visibility = "hidden";
			_t.nextButtonNode.style.visibility = "hidden";
		}
		if(!groupData.leaveTitle){
			_t.textNode.innerHTML = groupData.title;
		}
		_t._ready(src);
	},
	
	_ready: function(src){
		// summary: A function to trigger all 'real' showing of some src
		
		var _t = this;
		
		// listen for 404's:
		_t._imgError = dojo.connect(_t.imgNode, "error", _t, function(){
			dojo.disconnect(_t._imgError);
			// trigger the above onload with a new src:
			_t.imgNode.src = _t.errorImg;
			_t.textNode.innerHTML = _t.errorMessage;
		});
		
		// connect to the onload of the image
		_t._imgConnect = dojo.connect(_t.imgNode, "load", _t, function(e){
			_t.resizeTo({
				w: _t.imgNode.width,
				h: _t.imgNode.height,
				duration:_t.duration
			});
			// cleanup
			dojo.disconnect(_t._imgConnect);
			if(_t._imgError){
				dojo.disconnect(_t._imgError);
			}
		});
		
		_t.imgNode.src = src;
	},

	_nextImage: function(){
		// summary: Load next image in group
		if(!this.inGroup){ return; }
		if(this._index + 1 < this.inGroup.length){
			this._index++;
		}else{
			this._index = 0;
		}
		this._loadImage();
	},

	_prevImage: function(){
		// summary: Load previous image in group
		if(this.inGroup){
			if(this._index == 0){
				this._index = this.inGroup.length - 1;
			}else{
				this._index--;
			}
			this._loadImage();
		}
	},

	_loadImage: function(){
		// summary: Do the prep work before we can show another image
		this._loadingAnim.play(1);
	},

	_prepNodes: function(){
		// summary: A localized hook to accompany _loadImage
		this._imageReady = false;
		if(this.inGroup && this.inGroup[this._index]){
			this.show({
				href: this.inGroup[this._index].href,
				title: this.inGroup[this._index].title
			});
		}else{
			this.show({
				title: this.errorMessage,
				href: this.errorImg
			});
		}
		
	},

	_calcTitleSize: function(){
		var sizes = dojo.map(dojo.query("> *", this.titleNode).position(), function(s){ return s.h; });
		return { h: Math.max.apply(Math, sizes) };
	},
	
	resizeTo: function(/* Object */size, forceTitle){
		// summary: Resize our dialog container, and fire _showImage
		
		var adjustSize = dojo.boxModel == "border-box" ?
			dojo._getBorderExtents(this.domNode).w : 0,
			titleSize = forceTitle || this._calcTitleSize()
		;
		
		this._lastTitleSize = titleSize;
		
		if(this.adjust &&
			(size.h + titleSize.h + adjustSize + 80 > this._vp.h ||
			 size.w + adjustSize + 60 > this._vp.w
			)
		){
			this._lastSize = size;
			size = this._scaleToFit(size);
		}
		this._currentSize = size;
		
		var _sizeAnim = dojox.fx.sizeTo({
			node: this.containerNode,
			duration: size.duration||this.duration,
			width: size.w + adjustSize,
			height: size.h + titleSize.h + adjustSize
		});
		this.connect(_sizeAnim, "onEnd", "_showImage");
		_sizeAnim.play(15);
	},

	_scaleToFit: function(/* Object */size){
		// summary: resize an image to fit within the bounds of the viewport
		// size: Object
		//		The 'size' object passed around for this image

		var ns = {},   // New size
			nvp = {
				w: this._vp.w - 80,
				h: this._vp.h - 60 - this._lastTitleSize.h
			};	// New viewport

		// Calculate aspect ratio
		var viewportAspect = nvp.w / nvp.h,
			imageAspect = size.w / size.h;

		// Calculate new image size
		if(imageAspect >= viewportAspect){
			ns.h = nvp.w / imageAspect;
			ns.w = nvp.w;
		}else{
			ns.w = imageAspect * nvp.h;
			ns.h = nvp.h;
		}

		// we actually have to style this image, it's too big
		this._wasStyled = true;
		this._setImageSize(ns);

		ns.duration = size.duration;
		return ns; // Object
	},
	
	_setImageSize: function(size){
		// summary: Reset the image size to some actual size.
		var s = this.imgNode;
		s.height = size.h;
		s.width = size.w;
	},

	// clobber inherited function, it is useless.
	_size: function(){},
	
	_position: function(/* Event */e){
		// summary: we want to know the viewport size any time it changes
		this._vp = dojo.window.getBox();
		this.inherited(arguments);
		
		// determine if we need to scale up or down, if at all.
		if(e && e.type == "resize"){
			if(this._wasStyled){
				this._setImageSize(this._lastSize);
				this.resizeTo(this._lastSize);
			}else{
				if(this.imgNode.height + 80 > this._vp.h || this.imgNode.width + 60 > this._vp.h){
					this.resizeTo({
						w: this.imgNode.width, h: this.imgNode.height
					});
				}
			}
		}
	},

	_showImage: function(){
		// summary: Fade in the image, and fire showNav
		this._showImageAnim.play(1);
	},

	_showNav: function(){
		// summary: Fade in the footer, and setup our connections.
		var titleSizeNow = dojo.marginBox(this.titleNode);
		if(titleSizeNow.h > this._lastTitleSize.h){
			this.resizeTo(this._wasStyled ? this._lastSize : this._currentSize, titleSizeNow);
		}else{
			this._showNavAnim.play(1);
		}
	},

	hide: function(){
		// summary: Hide the Master Lightbox
		dojo.fadeOut({
			node: this.titleNode,
			duration: 200,
			// #5112 - if you _don't_ change the .src, safari will
			// _never_ fire onload for this image
			onEnd: dojo.hitch(this, function(){
				this.imgNode.src = this._blankGif;
			})
		}).play(5);

		this.inherited(arguments);

		this.inGroup = null;
		this._index = null;
	},

	addImage: function(child, group){
		// summary: Add an image to this Master Lightbox
		//
		// child: Object
		//		The image information to add.
		//		href: String - link to image (required)
		//		title: String - title to display
		//
		// group: String?
		//		attach to group of similar tag or null for individual image instance
		var g = group;
		if(!child.href){ return; }
		if(g){
			if(!this._groups[g]){
				this._groups[g] = [];
			}
			this._groups[g].push(child);
		}else{ this._groups["XnoGroupX"].push(child); }
	},

	removeImage: function(/* Widget */child){
		// summary: Remove an image instance from this LightboxDialog.
		// child: Object
		//		A reference to the Lightbox child that was added (or an object literal)
		//		only the .href member is compared for uniqueness. The object may contain
		//		a .group member as well.
		
		var g = child.group || "XnoGroupX";
		dojo.every(this._groups[g], function(item, i, ar){
			if(item.href == child.href){
				ar.splice(i, 1);
				return false;
			}
			return true;
		});
	},
	
	removeGroup: function(group){
		// summary: Remove all images in a passed group
		if(this._groups[group]){ this._groups[group] = []; }
	},

	_handleKey: function(/* Event */e){
		// summary: Handle keyboard navigation internally
		if(!this.open){ return; }

		var dk = dojo.keys;
		switch(e.charOrCode){
			
			case dk.ESCAPE:
				this.hide();
				break;

			case dk.DOWN_ARROW:
			case dk.RIGHT_ARROW:
			case 78: // key "n"
				this._nextImage();
				break;

			case dk.UP_ARROW:
			case dk.LEFT_ARROW:
			case 80: // key "p"
				this._prevImage();
				break;
		}
	},
		
	_makeAnims: function(){
		// summary: make and cleanup animation and animation connections
		
		dojo.forEach(this._animConnects, dojo.disconnect);
		this._animConnects = [];
		this._showImageAnim = dojo.fadeIn({
				node: this.imgNode,
				duration: this.duration
			});
		this._animConnects.push(dojo.connect(this._showImageAnim, "onEnd", this, "_showNav"));
		this._loadingAnim = dojo.fx.combine([
				dojo.fadeOut({ node:this.imgNode, duration:175 }),
				dojo.fadeOut({ node:this.titleNode, duration:175 })
			]);
		this._animConnects.push(dojo.connect(this._loadingAnim, "onEnd", this, "_prepNodes"));
		this._showNavAnim = dojo.fadeIn({ node: this.titleNode, duration:225 });
	},
	
	onClick: function(groupData){
		// summary: a stub function, called with the currently displayed image as the only argument
	},
	
	_onImageClick: function(e){
		if(e && e.target == this.imgNode){
			this.onClick(this._lastGroup);
			// also fire the onclick for the Lightbox widget which triggered, if you
			// aren't working directly with the LBDialog
			if(this._lastGroup.declaredClass){
				this._lastGroup.onClick(this._lastGroup);
			}
		}
	}
});
