dojo.provide("dojox.image.ThumbnailPicker");
dojo.experimental("dojox.image.ThumbnailPicker");
//
// dojox.image.ThumbnailPicker courtesy Shane O Sullivan, licensed under a Dojo CLA
//
// For a sample usage, see http://www.skynet.ie/~sos/photos.php
//
//	document topics.

dojo.require("dojox.fx.scroll"); // is optional, but don't want to dojo[require] it
dojo.require("dojo.fx.easing");

dojo.require("dojo.fx");
dojo.require("dijit._Widget");
dojo.require("dijit._Templated");

// FIXME: use CSS for size, thumbHeight, and thumbWidth

dojo.declare("dojox.image.ThumbnailPicker",
	[dijit._Widget, dijit._Templated],
	{
	// summary:
	//		A scrolling Thumbnail Picker widget

	// imageStore: Object
	//		A data store that implements the dojo.data Read API.
	imageStore: null,

	// request: Object
	//		A dojo.data Read API Request object.
	request: null,

	// size: Number
	//		Width or height in pixels, depending if horizontal or vertical.
	size: 500, 

	// thumbHeight: Number
	//		Default height of a thumbnail image
	thumbHeight: 75, 

	// thumbWidth: Number
	//		Default width of an image
	thumbWidth: 100, 

	// useLoadNotifier: Boolean
	//		Setting useLoadNotifier to true makes a colored DIV appear under each
	//		thumbnail image, which is used to display the loading status of each
	//		image in the data store.
	useLoadNotifier: false,

	// useHyperlink: boolean
	//		Setting useHyperlink to true causes a click on a thumbnail to open a link.
	useHyperlink: false,

	// hyperlinkTarget: String
	//		If hyperlinkTarget is set to "new", clicking on a thumb will open a new window
	//		If it is set to anything else, clicking a thumbnail will open the url in the
	//		current window.
	hyperlinkTarget: "new",

	// isClickable: Boolean
	//		When set to true, the cursor over a thumbnail changes.
	isClickable: true,

	// isScrollable: Boolean
	//		When true, uses smoothScroll to move between pages
	isScrollable: true,

	// isHorizontal: Boolean
	//		If true, the thumbnails are displayed horizontally. Otherwise they are displayed
	//		vertically
	isHorizontal: true,

	// autoLoad: Boolean
	autoLoad: true,

	// linkAttr: String
	//		The attribute name for accessing the url from the data store
	linkAttr: "link",
	
	// imageThumbAttr: String
	//		The attribute name for accessing the thumbnail image url from the data store
	imageThumbAttr: "imageUrlThumb",
	
	// imageLargeAttr: String
	//		The attribute name for accessing the large image url from the data store
	imageLargeAttr: "imageUrl",
	
	// pageSize: Number
	//		The number of images to request each time.
	pageSize: 20,
	
	// titleAttr: String
	//		The attribute name for accessing the title from the data store
	titleAttr: "title",
	
	templateString: dojo.cache("dojox.image", "resources/ThumbnailPicker.html"),
	
	// thumbs: Array
	//		Stores the image nodes for the thumbnails.
	_thumbs: [],
	
	// _thumbIndex: Number
	//		The index of the first thumbnail shown
	_thumbIndex: 0,
	
	// _maxPhotos: Number
	//		The total number of photos in the image store
	_maxPhotos: 0,
	
	// _loadedImages: Object
	//		Stores the indices of images that have been marked as loaded using the
	//		markImageLoaded function.
	_loadedImages: {},

	baseClass: "ThumbnailPicker",

	cellClass: "Thumbnail",

	postCreate: function(){
		// summary:
		//		Initializes styles and listeners

		this.inherited(arguments);
		
		this.pageSize = Number(this.pageSize);

		this._scrollerSize = this.size - (51 * 2);
		
		var sizeProp = this._sizeProperty = this.isHorizontal ? "width" : "height";
	
		// FIXME: do this via css? calculate the correct width for the widget
		dojo.style(this.outerNode, "textAlign","center");
		dojo.style(this.outerNode, sizeProp, this.size+"px");
	
		dojo.style(this.thumbScroller, sizeProp, this._scrollerSize + "px");
	
		//If useHyperlink is true, then listen for a click on a thumbnail, and
		//open the link
		if(this.useHyperlink){
			dojo.subscribe(this.getClickTopicName(), this, function(packet){
				var index = packet.index;
				var url = this.imageStore.getValue(packet.data,this.linkAttr);
				
				//If the data item doesn't contain a URL, do nothing
				if(!url){return;}
				
				if(this.hyperlinkTarget == "new"){
					window.open(url);
				}else{
					window.location = url;
				}
			});
		}
	
		if(this.isClickable){
			dojo.addClass(this.thumbsNode, "thumbClickable");
		}
		this._totalSize = 0;

		var classExt = this.isHorizontal ? "Horiz" : "Vert";
	
		// FIXME: can we setup a listener around the whole element and determine based on e.target?
		dojo.addClass(this.navPrev, "prev" + classExt);
		dojo.addClass(this.navNext, "next" + classExt);
		dojo.addClass(this.thumbsNode, "thumb"+classExt);
		dojo.addClass(this.outerNode, "thumb"+classExt);

		dojo.attr(this.navNextImg, "src", this._blankGif);
		dojo.attr(this.navPrevImg, "src", this._blankGif);

		this.connect(this.navPrev, "onclick", "_prev");
		this.connect(this.navNext, "onclick", "_next");

		if(this.isHorizontal){
			this._sizeAttr = "offsetWidth";
			this._scrollAttr = "scrollLeft";
		}else{
			this._sizeAttr = "offsetHeight";
			this._scrollAttr = "scrollTop";
		}
	
		this._updateNavControls();
		
		this.init();
	},
	
	init: function(){
		// summary
		//		Loads first image
		if(this.isInitialized){
			return false;
		}
		this.isInitialized = true;

		if(this.imageStore && this.request){
			this._loadNextPage();
		}
		return true;
	},

	getClickTopicName: function(){
		// summary:
		//		Returns the name of the dojo topic that can be
		//		subscribed to in order to receive notifications on
		//		which thumbnail was selected.
		return this.id + "/select"; // String
	},

	getShowTopicName: function(){
		// summary:
		//		Returns the name of the dojo topic that can be
		//		subscribed to in order to receive notifications on
		//		which thumbnail is now visible
		return this.id + "/show"; // String
	},

	setDataStore: function(dataStore, request, /*optional*/paramNames){
		// summary:
		//		Sets the data store and request objects to read data from.
		// dataStore:
		//		An implementation of the dojo/data/api/Read API. This accesses the image
		//		data.
		// request:
		//		An implementation of the dojo/data/api/Request API. This specifies the
		//		query and paging information to be used by the data store
		// paramNames:
		//		An object defining the names of the item attributes to fetch from the
		//		data store.  The four attributes allowed are 'linkAttr', 'imageLargeAttr',
		//		'imageThumbAttr' and 'titleAttr'
		this.reset();
	
		this.request = {
			query: {},
			start: request.start || 0,
			count: request.count || 10,
			onBegin: dojo.hitch(this, function(total){
				this._maxPhotos = total;
			})
		};
	
		if(request.query){ dojo.mixin(this.request.query, request.query);}
	
		if(paramNames){
			dojo.forEach(["imageThumbAttr", "imageLargeAttr", "linkAttr", "titleAttr"], function(attrName){
				if(paramNames[attrName]){ this[attrName] = paramNames[attrName]; }
			}, this);
		}
		
		this.request.start = 0;
		this.request.count = this.pageSize;
		this.imageStore = dataStore;
		this._loadInProgress = false;
		if(!this.init()){this._loadNextPage();}
	},

	reset: function(){
		// summary:
		//		Resets the widget back to its original state.
		this._loadedImages = {};
		dojo.forEach(this._thumbs, function(img){
			if(img && img.parentNode){
				dojo.destroy(img);
			}
		});
	
		this._thumbs = [];
		this.isInitialized = false;
		this._noImages = true;
	},
	
	isVisible: function(index) {
		// summary:
		//		Returns true if the image at the specified index is currently visible. False otherwise.
		var img = this._thumbs[index];
		if(!img){return false;}
		var pos = this.isHorizontal ? "offsetLeft" : "offsetTop";
		var size = this.isHorizontal ? "offsetWidth" : "offsetHeight";
		var scrollAttr = this.isHorizontal ? "scrollLeft" : "scrollTop";
		var offset = img[pos] - this.thumbsNode[pos];
		return (offset >= this.thumbScroller[scrollAttr]
			&& offset + img[size] <= this.thumbScroller[scrollAttr] + this._scrollerSize);
	},
	
	resize: function(dim){
		var sizeParam = this.isHorizontal ? "w": "h";

		var total = 0;

		if(this._thumbs.length > 0 && dojo.marginBox(this._thumbs[0]).w == 0){
			// Skip the resize if the widget is not visible
			return;
		}

		// Calculate the complete size of the thumbnails
		dojo.forEach(this._thumbs, dojo.hitch(this, function(imgContainer){
			var mb = dojo.marginBox(imgContainer.firstChild);
			var size = mb[sizeParam];
			total += (Number(size) + 10);
			
			if(this.useLoadNotifier && mb.w > 0){
				dojo.style(imgContainer.lastChild, "width", (mb.w - 4) + "px");
			}
			dojo.style(imgContainer, "width", mb.w + "px");
		}));

		dojo.style(this.thumbsNode, this._sizeProperty, total + "px");
		this._updateNavControls();
	},
	
	_next: function() {
		// summary:
		//		Displays the next page of images
		var pos = this.isHorizontal ? "offsetLeft" : "offsetTop";
		var size = this.isHorizontal ? "offsetWidth" : "offsetHeight";
		var baseOffset = this.thumbsNode[pos];
		var firstThumb = this._thumbs[this._thumbIndex];
		var origOffset = firstThumb[pos] - baseOffset;
	
		var index = -1, img;
	
		for(var i = this._thumbIndex + 1; i < this._thumbs.length; i++){
			img = this._thumbs[i];
			if(img[pos] - baseOffset + img[size] - origOffset > this._scrollerSize){
				this._showThumbs(i);
				return;
			}
		}
	},

	_prev: function(){
		// summary:
		//		Displays the next page of images
		if(this.thumbScroller[this.isHorizontal ? "scrollLeft" : "scrollTop"] == 0){return;}
		var pos = this.isHorizontal ? "offsetLeft" : "offsetTop";
		var size = this.isHorizontal ? "offsetWidth" : "offsetHeight";
	
		var firstThumb = this._thumbs[this._thumbIndex];
		var origOffset = firstThumb[pos] - this.thumbsNode[pos];
	
		var index = -1, img;
	
		for(var i = this._thumbIndex - 1; i > -1; i--) {
			img = this._thumbs[i];
			if(origOffset - img[pos] > this._scrollerSize){
				this._showThumbs(i + 1);
				return;
			}
		}
		this._showThumbs(0);
	},

	_checkLoad: function(img, index){
		// summary:
		//		Checks if an image is loaded.
		dojo.publish(this.getShowTopicName(), [{index:index}]);
		this._updateNavControls();
		this._loadingImages = {};
	
		this._thumbIndex = index;
	
		//If we have not already requested the data from the store, do so.
		if(this.thumbsNode.offsetWidth - img.offsetLeft < (this._scrollerSize * 2)){
			this._loadNextPage();
		}
	},

	_showThumbs: function(index){
		// summary:
		//		Displays thumbnail images, starting at position 'index'
		// index: Number
		//		The index of the first thumbnail

//FIXME: When is this be called with an invalid index?  Do we need this check at all?
//		if(typeof index != "number"){ index = this._thumbIndex; }
		index = Math.min(Math.max(index, 0), this._maxPhotos);
		
		if(index >= this._maxPhotos){ return; }
		
		var img = this._thumbs[index];
		if(!img){ return; }
		
		var left = img.offsetLeft - this.thumbsNode.offsetLeft;
		var top = img.offsetTop - this.thumbsNode.offsetTop;
		var offset = this.isHorizontal ? left : top;
				
		if(	(offset >= this.thumbScroller[this._scrollAttr]) &&
			(offset + img[this._sizeAttr] <= this.thumbScroller[this._scrollAttr] + this._scrollerSize)
		){
			// FIXME: WTF is this checking for?
			return;
		}
		
		
		if(this.isScrollable){
			var target = this.isHorizontal ? {x: left, y: 0} : { x:0, y:top};
			dojox.fx.smoothScroll({
				target: target,
				win: this.thumbScroller,
				duration:300,
				easing:dojo.fx.easing.easeOut,
				onEnd: dojo.hitch(this, "_checkLoad", img, index)
			}).play(10);
		}else{
			if(this.isHorizontal){
				this.thumbScroller.scrollLeft = left;
			}else{
				this.thumbScroller.scrollTop = top;
			}
			this._checkLoad(img, index);
		}
	},
	
	markImageLoaded: function(index){
		// summary:
		//		Changes a visual cue to show the image is loaded
		// description:
		//		If 'useLoadNotifier' is set to true, then a visual cue is
		//		given to state whether the image is loaded or not.	Calling this function
		//		marks an image as loaded.
		var thumbNotifier = dojo.byId("loadingDiv_"+this.id+"_"+index);
		if(thumbNotifier){this._setThumbClass(thumbNotifier, "thumbLoaded");}
		this._loadedImages[index] = true;
	},

	_setThumbClass: function(thumb, className){
		// summary:
		//		Adds a CSS class to a thumbnail, only if 'autoLoad' is true
		// thumb: DomNode
		//		The thumbnail DOM node to set the class on
		// className: String
		//		The CSS class to add to the DOM node.
		if(!this.autoLoad){ return; }
		dojo.addClass(thumb, className);
	},
                                                 
	_loadNextPage: function(){
		// summary:
		//		Loads the next page of thumbnail images
		if(this._loadInProgress){return;}
		this._loadInProgress = true;
		var start = this.request.start + (this._noImages ? 0 : this.pageSize);
		
		var pos = start;
		while(pos < this._thumbs.length && this._thumbs[pos]){pos ++;}
		
		var store = this.imageStore;
		
		//Define the function to call when the items have been
		//returned from the data store.
		var complete = function(items, request){
			if(store != this.imageStore){
				// If the store has been changed, ignore this callback.
				return;
			}
			if(items && items.length){
				var itemCounter = 0;
				var loadNext = dojo.hitch(this, function(){
					if(itemCounter >= items.length){
						this._loadInProgress = false;
						return;
					}
					var counter = itemCounter++;

					this._loadImage(items[counter], pos + counter, loadNext);
				});
				loadNext();

				//Show or hide the navigation arrows on the thumbnails,
				//depending on whether or not the widget is at the start,
				//end, or middle of the list of images.
				this._updateNavControls();
			}else{
				this._loadInProgress = false;
			}
		};
	
		//Define the function to call if the store reports an error.
		var error = function(){
			this._loadInProgress = false;
			console.log("Error getting items");
		};

		this.request.onComplete = dojo.hitch(this, complete);
		this.request.onError = dojo.hitch(this, error);
	
		//Increment the start parameter. This is the dojo.data API's
		//version of paging.
		this.request.start = start;
		this._noImages = false;
		
		//Execute the request for data.
		this.imageStore.fetch(this.request);
	},

	_loadImage: function(data, index, callback){
		// summary:
		//		Loads an image.

		var store = this.imageStore;
		var url = store.getValue(data,this.imageThumbAttr);
		
		var imgContainer = dojo.create("div", {
			id: "img_" + this.id + "_" + index,
			"class": this.cellClass
		});
		
		var img = dojo.create("img", {}, imgContainer);
		img._index = index;
		img._data = data;
	
		this._thumbs[index] = imgContainer;
		var loadingDiv;
		if(this.useLoadNotifier){
			loadingDiv = dojo.create("div", {
				id: "loadingDiv_" + this.id+"_" + index
			}, imgContainer);
	
			//If this widget was previously told that the main image for this
			//thumb has been loaded, make the loading indicator transparent.
			this._setThumbClass(loadingDiv,
				this._loadedImages[index] ? "thumbLoaded":"thumbNotifier");
		}
		var size = dojo.marginBox(this.thumbsNode);
		var defaultSize;
		var sizeParam;
		if(this.isHorizontal){
			defaultSize = this.thumbWidth;
			sizeParam = 'w';
		} else{
			defaultSize = this.thumbHeight;
			sizeParam = 'h';
		}
		size = size[sizeParam];
		var sl = this.thumbScroller.scrollLeft, st = this.thumbScroller.scrollTop;

		dojo.style(this.thumbsNode, this._sizeProperty, (size + defaultSize + 20) + "px");

		//Remember the scroll values, as changing the size can alter them
		this.thumbScroller.scrollLeft = sl;
		this.thumbScroller.scrollTop = st;
		this.thumbsNode.appendChild(imgContainer);
	
		dojo.connect(img, "onload", this, dojo.hitch(this, function(){
			if(store != this.imageStore){
				// If the store has changed, ignore this load event
				return false;
			}
			this.resize();
						
			// Have to use a timeout here to prevent a call stack that gets
			// so deep that IE throws stack overflow errors
			setTimeout(callback, 0);
			return false;
		}));
	
		dojo.connect(img, "onclick", this, function(evt){
			dojo.publish(this.getClickTopicName(),	[{
				index: evt.target._index,
				data: evt.target._data,
				url: img.getAttribute("src"),
				largeUrl: this.imageStore.getValue(data,this.imageLargeAttr),
				title: this.imageStore.getValue(data,this.titleAttr),
				link: this.imageStore.getValue(data,this.linkAttr)
			}]);
			//
			dojo.query("." + this.cellClass, this.thumbsNode).removeClass(this.cellClass + "Selected");
			dojo.addClass(evt.target.parentNode, this.cellClass + "Selected");
			return false;
		});
		dojo.addClass(img, "imageGalleryThumb");
		img.setAttribute("src", url);
		var title = this.imageStore.getValue(data, this.titleAttr);
		if(title){ img.setAttribute("title",title); }
		this._updateNavControls();
	
	},

	_updateNavControls: function(){
		// summary:
		//		Updates the navigation controls to hide/show them when at
		//		the first or last images.
		var change = function(node, add){
			var fn = add ? "addClass" : "removeClass";
			dojo[fn](node,"enabled");
			dojo[fn](node,"thumbClickable");
		};
		
		var pos = this.isHorizontal ? "scrollLeft" : "scrollTop";
		var size = this.isHorizontal ? "offsetWidth" : "offsetHeight";
		change(this.navPrev, (this.thumbScroller[pos] > 0));
		
		var addClass = (this.thumbScroller[pos] + this._scrollerSize < this.thumbsNode[size]);
		change(this.navNext, addClass);
	}
});
