/* 	Module:
		PhotoGallery

	Author:
		Neil Jenkins - http://www.nmjenkins.com
		
	Version:
		1.1 (2008-08-27)
		
	Version History:
		1.1  (2008-08-27) Update to Mootools 1.2 and refine
		1.03 (2008-04-24) Change to 'keydown' to allow Safari to use keyboard shortcuts
		1.02 (2008-04-22) Fix bug where hitting next whilst an image was loading caused
						  the same image to be continuously displayed.
		1.01 (2008-04-16) Allow preference to no resize images
		1.0  (2008-02-01) Initial release

	License:
		GNU GPL 2.0: http://creativecommons.org/licenses/GPL/2.0/
	
	Description:
		Javascript module to build a kickass photo gallery from a Flickr account.
		
	Usage:
		Attach this file to a suitable HTML page.

	Dependencies:
		mootools: http://mootools.net
		HistoryManager
		Flickr
		Stylesheet

	Notes:
		Supports Firefox 1.5+, Safari 2+, Opera 9+ and IE 6+
		Other browsers may work but are untested.
*/



// Insert your Flickr API key here.
FlickrAPI.initialise("fba4825365098b5c4d304f5216004e7d");

// Insert your Flickr nsid here:
var nsid = "73912292@N00"; //Test id: 45492092@N00

// Preference: Stop the browser to resize images. You may wish to set this for Windows machines as most
// Windows browsers don't interpolate images properly so it looks a bit odd.
var noResize = false; //e.g. navigator.userAgent.test(/Windows/);



/* ----------------------------------------------------------------- */

// DON'T TOUCH ANYTHING BELOW HERE UNLESS YOU KNOW WHAT YOU ARE DOING!

/* ----------------------------------------------------------------- */

//MooTools More v1.2.4.4, <http://mootools.net/more>. Copyright (c) 2006-2009 Aaron Newton <http://clientcide.com/>, Valerio Proietti <http://mad4milk.net> & the MooTools team <http://mootools.net/developers>, MIT Style License.
// Fx.Scroll, Drag, Hash.Cookie, Assets and Slider

MooTools.More={version:"1.2.4.4",build:"6f6057dc645fdb7547689183b2311063bd653ddf"};Class.Mutators.Binds=function(a){return a;};Class.Mutators.initialize=function(a){return function(){$splat(this.Binds).each(function(b){var c=this[b];
if(c){this[b]=c.bind(this);}},this);return a.apply(this,arguments);};};Element.implement({measure:function(e){var g=function(h){return !!(!h||h.offsetHeight||h.offsetWidth);
};if(g(this)){return e.apply(this);}var d=this.getParent(),f=[],b=[];while(!g(d)&&d!=document.body){b.push(d.expose());d=d.getParent();}var c=this.expose();
var a=e.apply(this);c();b.each(function(h){h();});return a;},expose:function(){if(this.getStyle("display")!="none"){return $empty;}var a=this.style.cssText;
this.setStyles({display:"block",position:"absolute",visibility:"hidden"});return function(){this.style.cssText=a;}.bind(this);},getDimensions:function(a){a=$merge({computeSize:false},a);
var f={};var d=function(g,e){return(e.computeSize)?g.getComputedSize(e):g.getSize();};var b=this.getParent("body");if(b&&this.getStyle("display")=="none"){f=this.measure(function(){return d(this,a);
});}else{if(b){try{f=d(this,a);}catch(c){}}else{f={x:0,y:0};}}return $chk(f.x)?$extend(f,{width:f.x,height:f.y}):$extend(f,{x:f.width,y:f.height});},getComputedSize:function(a){a=$merge({styles:["padding","border"],plains:{height:["top","bottom"],width:["left","right"]},mode:"both"},a);
var c={width:0,height:0};switch(a.mode){case"vertical":delete c.width;delete a.plains.width;break;case"horizontal":delete c.height;delete a.plains.height;
break;}var b=[];$each(a.plains,function(g,f){g.each(function(h){a.styles.each(function(i){b.push((i=="border")?i+"-"+h+"-width":i+"-"+h);});});});var e={};
b.each(function(f){e[f]=this.getComputedStyle(f);},this);var d=[];$each(a.plains,function(g,f){var h=f.capitalize();c["total"+h]=c["computed"+h]=0;g.each(function(i){c["computed"+i.capitalize()]=0;
b.each(function(k,j){if(k.test(i)){e[k]=e[k].toInt()||0;c["total"+h]=c["total"+h]+e[k];c["computed"+i.capitalize()]=c["computed"+i.capitalize()]+e[k];}if(k.test(i)&&f!=k&&(k.test("border")||k.test("padding"))&&!d.contains(k)){d.push(k);
c["computed"+h]=c["computed"+h]-e[k];}});});});["Width","Height"].each(function(g){var f=g.toLowerCase();if(!$chk(c[f])){return;}c[f]=c[f]+this["offset"+g]+c["computed"+g];
c["total"+g]=c[f]+c["total"+g];delete c["computed"+g];},this);return $extend(e,c);}});Fx.Scroll=new Class({Extends:Fx,options:{offset:{x:0,y:0},wheelStops:true},initialize:function(b,a){this.element=this.subject=document.id(b);
this.parent(a);var d=this.cancel.bind(this,false);if($type(this.element)!="element"){this.element=document.id(this.element.getDocument().body);}var c=this.element;
if(this.options.wheelStops){this.addEvent("start",function(){c.addEvent("mousewheel",d);},true);this.addEvent("complete",function(){c.removeEvent("mousewheel",d);
},true);}},set:function(){var a=Array.flatten(arguments);if(Browser.Engine.gecko){a=[Math.round(a[0]),Math.round(a[1])];}this.element.scrollTo(a[0],a[1]);
},compute:function(c,b,a){return[0,1].map(function(d){return Fx.compute(c[d],b[d],a);});},start:function(c,g){if(!this.check(c,g)){return this;}var e=this.element.getScrollSize(),b=this.element.getScroll(),d={x:c,y:g};
for(var f in d){var a=e[f];if($chk(d[f])){d[f]=($type(d[f])=="number")?d[f]:a;}else{d[f]=b[f];}d[f]+=this.options.offset[f];}return this.parent([b.x,b.y],[d.x,d.y]);
},toTop:function(){return this.start(false,0);},toLeft:function(){return this.start(0,false);},toRight:function(){return this.start("right",false);},toBottom:function(){return this.start(false,"bottom");
},toElement:function(b){var a=document.id(b).getPosition(this.element);return this.start(a.x,a.y);},scrollIntoView:function(c,e,d){e=e?$splat(e):["x","y"];
var h={};c=document.id(c);var f=c.getPosition(this.element);var i=c.getSize();var g=this.element.getScroll();var a=this.element.getSize();var b={x:f.x+i.x,y:f.y+i.y};
["x","y"].each(function(j){if(e.contains(j)){if(b[j]>g[j]+a[j]){h[j]=b[j]-a[j];}if(f[j]<g[j]){h[j]=f[j];}}if(h[j]==null){h[j]=g[j];}if(d&&d[j]){h[j]=h[j]+d[j];
}},this);if(h.x!=g.x||h.y!=g.y){this.start(h.x,h.y);}return this;},scrollToCenter:function(c,e,d){e=e?$splat(e):["x","y"];c=$(c);var h={},f=c.getPosition(this.element),i=c.getSize(),g=this.element.getScroll(),a=this.element.getSize(),b={x:f.x+i.x,y:f.y+i.y};
["x","y"].each(function(j){if(e.contains(j)){h[j]=f[j]-(a[j]-i[j])/2;}if(h[j]==null){h[j]=g[j];}if(d&&d[j]){h[j]=h[j]+d[j];}},this);if(h.x!=g.x||h.y!=g.y){this.start(h.x,h.y);
}return this;}});var Drag=new Class({Implements:[Events,Options],options:{snap:6,unit:"px",grid:false,style:true,limit:false,handle:false,invert:false,preventDefault:false,stopPropagation:false,modifiers:{x:"left",y:"top"}},initialize:function(){var b=Array.link(arguments,{options:Object.type,element:$defined});
this.element=document.id(b.element);this.document=this.element.getDocument();this.setOptions(b.options||{});var a=$type(this.options.handle);this.handles=((a=="array"||a=="collection")?$$(this.options.handle):document.id(this.options.handle))||this.element;
this.mouse={now:{},pos:{}};this.value={start:{},now:{}};this.selection=(Browser.Engine.trident)?"selectstart":"mousedown";this.bound={start:this.start.bind(this),check:this.check.bind(this),drag:this.drag.bind(this),stop:this.stop.bind(this),cancel:this.cancel.bind(this),eventStop:$lambda(false)};
this.attach();},attach:function(){this.handles.addEvent("mousedown",this.bound.start);return this;},detach:function(){this.handles.removeEvent("mousedown",this.bound.start);
return this;},start:function(c){if(c.rightClick){return;}if(this.options.preventDefault){c.preventDefault();}if(this.options.stopPropagation){c.stopPropagation();
}this.mouse.start=c.page;this.fireEvent("beforeStart",this.element);var a=this.options.limit;this.limit={x:[],y:[]};for(var d in this.options.modifiers){if(!this.options.modifiers[d]){continue;
}if(this.options.style){this.value.now[d]=this.element.getStyle(this.options.modifiers[d]).toInt();}else{this.value.now[d]=this.element[this.options.modifiers[d]];
}if(this.options.invert){this.value.now[d]*=-1;}this.mouse.pos[d]=c.page[d]-this.value.now[d];if(a&&a[d]){for(var b=2;b--;b){if($chk(a[d][b])){this.limit[d][b]=$lambda(a[d][b])();
}}}}if($type(this.options.grid)=="number"){this.options.grid={x:this.options.grid,y:this.options.grid};}this.document.addEvents({mousemove:this.bound.check,mouseup:this.bound.cancel});
this.document.addEvent(this.selection,this.bound.eventStop);},check:function(a){if(this.options.preventDefault){a.preventDefault();}var b=Math.round(Math.sqrt(Math.pow(a.page.x-this.mouse.start.x,2)+Math.pow(a.page.y-this.mouse.start.y,2)));
if(b>this.options.snap){this.cancel();this.document.addEvents({mousemove:this.bound.drag,mouseup:this.bound.stop});this.fireEvent("start",[this.element,a]).fireEvent("snap",this.element);
}},drag:function(a){if(this.options.preventDefault){a.preventDefault();}this.mouse.now=a.page;for(var b in this.options.modifiers){if(!this.options.modifiers[b]){continue;
}this.value.now[b]=this.mouse.now[b]-this.mouse.pos[b];if(this.options.invert){this.value.now[b]*=-1;}if(this.options.limit&&this.limit[b]){if($chk(this.limit[b][1])&&(this.value.now[b]>this.limit[b][1])){this.value.now[b]=this.limit[b][1];
}else{if($chk(this.limit[b][0])&&(this.value.now[b]<this.limit[b][0])){this.value.now[b]=this.limit[b][0];}}}if(this.options.grid[b]){this.value.now[b]-=((this.value.now[b]-(this.limit[b][0]||0))%this.options.grid[b]);
}if(this.options.style){this.element.setStyle(this.options.modifiers[b],this.value.now[b]+this.options.unit);}else{this.element[this.options.modifiers[b]]=this.value.now[b];
}}this.fireEvent("drag",[this.element,a]);},cancel:function(a){this.document.removeEvent("mousemove",this.bound.check);this.document.removeEvent("mouseup",this.bound.cancel);
if(a){this.document.removeEvent(this.selection,this.bound.eventStop);this.fireEvent("cancel",this.element);}},stop:function(a){this.document.removeEvent(this.selection,this.bound.eventStop);
this.document.removeEvent("mousemove",this.bound.drag);this.document.removeEvent("mouseup",this.bound.stop);if(a){this.fireEvent("complete",[this.element,a]);
}}});Element.implement({makeResizable:function(a){var b=new Drag(this,$merge({modifiers:{x:"width",y:"height"}},a));this.store("resizer",b);return b.addEvent("drag",function(){this.fireEvent("resize",b);
}.bind(this));}});var Slider=new Class({Implements:[Events,Options],Binds:["clickedElement","draggedKnob","scrolledElement"],options:{onTick:function(a){if(this.options.snap){a=this.toPosition(this.step);
}this.knob.setStyle(this.property,a);},initialStep:0,snap:false,offset:0,range:false,wheel:false,steps:100,mode:"horizontal"},initialize:function(f,a,e){this.setOptions(e);
this.element=document.id(f);this.knob=document.id(a);this.previousChange=this.previousEnd=this.step=-1;var g,b={},d={x:false,y:false};switch(this.options.mode){case"vertical":this.axis="y";
this.property="top";g="offsetHeight";break;case"horizontal":this.axis="x";this.property="left";g="offsetWidth";}this.full=this.element.measure(function(){this.half=this.knob[g]/2;
return this.element[g]-this.knob[g]+(this.options.offset*2);}.bind(this));this.min=$chk(this.options.range[0])?this.options.range[0]:0;this.max=$chk(this.options.range[1])?this.options.range[1]:this.options.steps;
this.range=this.max-this.min;this.steps=this.options.steps||this.full;this.stepSize=Math.abs(this.range)/this.steps;this.stepWidth=this.stepSize*this.full/Math.abs(this.range);
this.knob.setStyle("position","relative").setStyle(this.property,this.options.initialStep?this.toPosition(this.options.initialStep):-this.options.offset);
d[this.axis]=this.property;b[this.axis]=[-this.options.offset,this.full-this.options.offset];var c={snap:0,limit:b,modifiers:d,onDrag:this.draggedKnob,onStart:this.draggedKnob,onBeforeStart:(function(){this.isDragging=true;
}).bind(this),onCancel:function(){this.isDragging=false;}.bind(this),onComplete:function(){this.isDragging=false;this.draggedKnob();this.end();}.bind(this)};
if(this.options.snap){c.grid=Math.ceil(this.stepWidth);c.limit[this.axis][1]=this.full;}this.drag=new Drag(this.knob,c);this.attach();},attach:function(){this.element.addEvent("mousedown",this.clickedElement);
if(this.options.wheel){this.element.addEvent("mousewheel",this.scrolledElement);}this.drag.attach();return this;},detach:function(){this.element.removeEvent("mousedown",this.clickedElement);
this.element.removeEvent("mousewheel",this.scrolledElement);this.drag.detach();return this;},set:function(a){if(!((this.range>0)^(a<this.min))){a=this.min;
}if(!((this.range>0)^(a>this.max))){a=this.max;}this.step=Math.round(a);this.checkStep();this.fireEvent("tick",this.toPosition(this.step));this.end();return this;
},clickedElement:function(c){if(this.isDragging||c.target==this.knob){return;}var b=this.range<0?-1:1;var a=c.page[this.axis]-this.element.getPosition()[this.axis]-this.half;
a=a.limit(-this.options.offset,this.full-this.options.offset);this.step=Math.round(this.min+b*this.toStep(a));this.checkStep();this.fireEvent("tick",a);
this.end();},scrolledElement:function(a){var b=(this.options.mode=="horizontal")?(a.wheel<0):(a.wheel>0);this.set(b?this.step-this.stepSize:this.step+this.stepSize);
a.stop();},draggedKnob:function(){var b=this.range<0?-1:1;var a=this.drag.value.now[this.axis];a=a.limit(-this.options.offset,this.full-this.options.offset);
this.step=Math.round(this.min+b*this.toStep(a));this.checkStep();},checkStep:function(){if(this.previousChange!=this.step){this.previousChange=this.step;
this.fireEvent("change",this.step);}},end:function(){if(this.previousEnd!==this.step){this.previousEnd=this.step;this.fireEvent("complete",this.step+"");
}},toStep:function(a){var b=(a+this.options.offset)*this.stepSize/this.full*this.steps;return this.options.steps?Math.round(b-=b%this.stepSize):b;},toPosition:function(a){return(this.full*Math.abs(this.min-a))/(this.steps*this.stepSize)-this.options.offset;
}});var Asset={javascript:function(f,d){d=$extend({onload:$empty,document:document,check:$lambda(true)},d);if(d.onLoad){d.onload=d.onLoad;}var b=new Element("script",{src:f,type:"text/javascript"});
var e=d.onload.bind(b),a=d.check,g=d.document;delete d.onload;delete d.check;delete d.document;b.addEvents({load:e,readystatechange:function(){if(["loaded","complete"].contains(this.readyState)){e();
}}}).set(d);if(Browser.Engine.webkit419){var c=(function(){if(!$try(a)){return;}$clear(c);e();}).periodical(50);}return b.inject(g.head);},css:function(b,a){return new Element("link",$merge({rel:"stylesheet",media:"screen",type:"text/css",href:b},a)).inject(document.head);
},image:function(c,b){b=$merge({onload:$empty,onabort:$empty,onerror:$empty},b);var d=new Image();var a=document.id(d)||new Element("img");["load","abort","error"].each(function(e){var g="on"+e;
var f=e.capitalize();if(b["on"+f]){b[g]=b["on"+f];}var h=b[g];delete b[g];d[g]=function(){if(!d){return;}if(!a.parentNode){a.width=d.width;a.height=d.height;
}d=d.onload=d.onabort=d.onerror=null;h.delay(1,a,a);a.fireEvent(e,a,1);};});d.src=a.src=c;if(d&&d.complete){d.onload.delay(1);}return a.set(b);},images:function(d,c){c=$merge({onComplete:$empty,onProgress:$empty,onError:$empty,properties:{}},c);
d=$splat(d);var a=[];var b=0;return new Elements(d.map(function(e){return Asset.image(e,$extend(c.properties,{onload:function(){c.onProgress.call(this,b,d.indexOf(e));
b++;if(b==d.length){c.onComplete();}},onerror:function(){c.onError.call(this,b,d.indexOf(e));b++;if(b==d.length){c.onComplete();}}}));}));}};Hash.Cookie=new Class({Extends:Cookie,options:{autoSave:true},initialize:function(b,a){this.parent(b,a);
this.load();},save:function(){var a=JSON.encode(this.hash);if(!a||a.length>4096){return false;}if(a=="{}"){this.dispose();}else{this.write(a);}return true;
},load:function(){this.hash=new Hash(JSON.decode(this.read(),true));return this;}});Hash.each(Hash.prototype,function(b,a){if(typeof b=="function"){Hash.Cookie.implement(a,function(){var c=b.apply(this.hash,arguments);
if(this.options.autoSave){this.save();}return c;});}});

/* 	Function: elementFromTemplate
		Creates and returns a new element from a template.
		Replaces dummy variables <%=var%> within the template with the correct values.
		
	Arguments:
		string: template id/reference
		object: an associative array of variable: value pairs.
		
	Notes:
		for clarity can optionally be used with the new keyword
*/

function elementFromTemplate(template, properties) {

	var html = $(template).innerHTML.split(/<%=|%>/);
	for (var i = 1; i < html.length; i+=2)
		html[i] = properties[html[i]];
	return new Element('div').set('html', html.join('')).getFirst();
}

/* ---------------------------------- */

window.addEvent('domready', function() {

	new Gallery();

});

/* galleryData structure:
	
	{
	albumList: [photoset_id],	
	album{photoset_id}: {
		title: String,
		description: String,
		keyPhotoSrc: String,
		keyPhotoID
		numberOfPhotos: Number,
		photos: imgData
		}
	}
	
	imgData structure:

	{
	photo'photoID': {
		id: String,
		title: String,
		description: String,
		src: String,
		next: String photoID,
		previous: String photoID,
		ratio: Number (width/height),
		variants: [{width: Number, height: Number, name: String}]
	},
	_firstImage: String photoid
	}
*/

/* All Flickr API calls should in theory be from this class */

var Gallery = new Class({
	
	Implements: Events,

	initialize: function() {
	
		var gallery = this;
		this._galleryData = {};

		this.albums = new Albums(this);
		this.grid = new Grid(this);
		this.displayPhoto = new DisplayPhoto(this);
		this.filmstrip = new Filmstrip(this);
		
		// Setup history management
		this.history = new HistoryManager();
		this.history.addEvent('onHistoryChange', function(hash) {

			var state = hash.split('/');

			var mode = state[0] || 'albums';
			var photoset_id = state[1];
			var image_id = state[2];
			
			gallery.changeMode(mode, photoset_id, image_id);
		});
		this.history.fireEvent('onHistoryChange', [this.history.getCurrentHash()]);
		
		// Add history state whenver mode changes
		this.addEvent('modeChange', function(mode, photoset_id, image_id) {
			if (mode == 'albums')
				this.history.addState('');
			else
				this.history.addState(mode + '/' + photoset_id + (image_id ? ('/' + image_id) : ''));
		}.bind(this));
		
		// Set up links to change mode.
		['albums', 'grid','filmstrip','single'].each(function(mode){
			$$('.mode' + mode.capitalize()).addEvent('click', function(event) {
				new Event(event).preventDefault();
				this.blur();
				gallery.changeMode(mode);
			});
		});
	},
	
	getAlbumList: function(callbackFunction) {
	
		if (this._galleryData.albumList)
			return callbackFunction(this._galleryData.albumList);

		FlickrAPI.get({method: 'flickr.photosets.getList', user_id: nsid}, function(data) {

			this._galleryData.albumList = [];

			data.photosets.photoset.forEach(function(album) {

				this._galleryData.albumList.push(album.id);
				this._galleryData['album' + album.id] = this._galleryData['album' + album.id] || {};
							
				$extend(this._galleryData['album' + album.id], {
					title: album.title._content,
					description: album.description._content,
					keyPhotoSrc: "http://farm" + album.farm + ".static.flickr.com/" + album.server + "/" +
								  album.primary + "_" + album.secret + ".jpg",
					keyPhotoID: album.primary,
					numberOfPhotos: parseInt(album.photos)
				});
			}, this);
			callbackFunction(this._galleryData.albumList);
		}.bind(this));
	},
	
	getAlbumData: function(photoset_id, callbackFunction) {

		if (this._galleryData['album' + photoset_id])
			return callbackFunction(this._galleryData['album' + photoset_id]);
		
		FlickrAPI.get({method: 'flickr.photosets.getInfo', photoset_id: photoset_id}, function(data) {

			var album = data.photoset;
			this._galleryData['album' + album.id] = this._galleryData['album' + album.id] || {};
						
			$extend(this._galleryData['album' + album.id], {
				title: album.title._content,
				description: album.description._content,
				keyPhotoSrc: "http://farm" + album.farm + ".static.flickr.com/" + album.server + "/" +
							  album.primary + "_" + album.secret + ".jpg",
				keyPhotoID: album.primary,
				numberOfPhotos: parseInt(album.photos)
			});
			callbackFunction(this._galleryData['album' + photoset_id]);
		}.bind(this));
	},

	getAlbumPhotos: function(photoset_id, callbackFunction) {
		
		if (this._galleryData['album' + photoset_id]
		 && this._galleryData['album' + photoset_id].photos
		 && this._galleryData['album' + photoset_id].photos._firstImage)
			return callbackFunction(this._galleryData['album' + photoset_id].photos);
		
		// Optional extra: date_taken
		FlickrAPI.get({method: 'flickr.photosets.getPhotos', photoset_id: photoset_id, extras: 'original_format'}, function(data) {

			this._galleryData['album' + photoset_id] = this._galleryData['album' + photoset_id] || {};
			var imageData = (this._galleryData['album' + photoset_id].photos = this._galleryData['album' + photoset_id].photos || {});
			
			data.photoset.photo.forEach(function(photo, i){
				imageData['photo' + photo.id] = imageData['photo' + photo.id] || {}
				$extend(imageData['photo' + photo.id], {
					id: photo.id,
					title: photo.title || '',
					srcBase: "http://farm" + photo.farm + ".static.flickr.com/" + photo.server + "/" +
							  photo.id + "_" + photo.secret,
					originalSrc: "http://farm" + photo.farm + ".static.flickr.com/" + photo.server + "/" +
							  photo.id + "_" + photo.originalsecret + "_o_d." + photo.originalformat,
					next: (i == data.photoset.photo.length-1) ? null : data.photoset.photo[i + 1].id,
					previous: (i == 0) ? null : data.photoset.photo[i - 1].id
				});
			});
			imageData._firstImage = data.photoset.photo[0].id;

			callbackFunction(this._galleryData['album' + photoset_id].photos);
		}.bind(this));
	},

	getImageData: function(photoset_id, image_id, callbackFunction) {

		if (this._galleryData['album' + photoset_id]
		 && this._galleryData['album' + photoset_id].photos
		 && this._galleryData['album' + photoset_id].photos['photo' + image_id]
		 && $defined(this._galleryData['album' + photoset_id].photos['photo' + image_id].description))
			return callbackFunction(this._galleryData['album' + photoset_id].photos['photo' + image_id]);

		FlickrAPI.get({method: 'flickr.photos.getInfo', photo_id: image_id}, function(data) {

			var photo = data.photo;

			var photoData = this._galleryData['album' + photoset_id] = (this._galleryData['album' + photoset_id] || {});
		 	photoData = (photoData.photos = photoData.photos || {});
		 	photoData = (photoData['photo' + photo.id] = photoData['photo' + photo.id] || {});

		 	$extend(photoData, {
				id: photo.id,
				title: photo.title._content || ' ',
				description: photo.description._content || ' ',
				srcBase: "http://farm" + photo.farm + ".static.flickr.com/" + photo.server + "/" +
						  photo.id + "_" + photo.secret,
				originalSrc: "http://farm" + photo.farm + ".static.flickr.com/" + photo.server + "/" +
						  photo.id + "_" + photo.originalsecret + "_o_d." + photo.originalformat
			});
			return callbackFunction(photoData);
		}.bind(this));
	},

	getSizes: function(photoset_id, image_id, callbackFunction) {
		if (this._galleryData['album' + photoset_id]
		 && this._galleryData['album' + photoset_id].photos
		 && this._galleryData['album' + photoset_id].photos['photo' + image_id]
		 && this._galleryData['album' + photoset_id].photos['photo' + image_id].variants)
		 	return callbackFunction(this._galleryData['album' + photoset_id].photos['photo' + image_id].variants);

		FlickrAPI.get({method: 'flickr.photos.getSizes', photo_id: image_id}, function(data) {
		
			this._galleryData['album' + photoset_id] = this._galleryData['album' + photoset_id] || {};
			this._galleryData['album' + photoset_id].photos = this._galleryData['album' + photoset_id].photos || {};
			this._galleryData['album' + photoset_id].photos['photo' + image_id] = this._galleryData['album' + photoset_id].photos['photo' + image_id] || {};
		
			var variants = (this._galleryData['album' + photoset_id].photos['photo' + image_id].variants = []);
			data.sizes.size.forEach(function(size) {
				variants.push({
					label: size.label,
					width: parseInt(size.width),
					height: parseInt(size.height),
					src: size.source
				});
			});

			variants.ratio = variants[variants.length-1].height / variants[variants.length-1].width;

		 	return callbackFunction(variants);
		 }.bind(this));
	},

	// Presumes image data already been called.
	getContext: function(photoset_id, image_id, callbackFunction) {
		var photo = this._galleryData['album' + photoset_id].photos['photo' + image_id];
		if (photo.next)
			return callbackFunction({next: photo.next, previous: photo.previous});
		FlickrAPI.get({method: 'flickr.photosets.getContext', photo_id: image_id, photoset_id: photoset_id}, function(data) {
			photo.next = data.nextphoto.id || null;
			photo.previous = data.prevphoto.id || null;
			callbackFunction({next: photo.next, previous: photo.previous})
		});
	},

	// Presumes getAlbumPhotos() has already been called.
	getFirst: function(photoset_id) {
		return this._galleryData['album' + photoset_id].photos._firstImage;
	},
	
	changeMode: function(mode, photoset_id, image_id) {
		
		// Allow a hash to be passed as a single argument
		// instead of the individual args.
		if (mode.charAt(0) == '#') {
			var state = mode.slice(2).split('/');
			mode = state[0], photoset_id = state[1], image_id = state[2];
		}
		
		if (this.mode == mode) {
			if (this.displayPhoto._active)
				this.displayPhoto.show(photoset_id, image_id);
			return;
		}

		document.body.className = mode.capitalize();

		if (mode != 'albums')
			photoset_id = photoset_id || this.displayPhoto._currentAlbum || this.grid._currentAlbum;

		if (mode == 'single' || mode == 'filmstrip')
			image_id = image_id || this.displayPhoto._currentPhoto || this.getFirst(photoset_id);

		$$('.modeSelector').removeClass('selected');
		$$('.mode' + mode.capitalize()).addClass('selected');

		this.fireEvent('modeChange', [mode, photoset_id, image_id]);

		this.mode = mode;
	}
});

var Albums = new Class({
	
	initialize: function(gallery) {
		this._gallery = gallery;
		this.title = document.title;
		gallery.addEvent('modeChange', function(mode) {
			document.title = this.title;
			if (mode != 'albums')
				return;
			if (!this.built)
				this.build();
		}.bind(this));
		
		$('albums').addEvent('click', function(event) {
			new Event(event);
			if ((event.target.nodeName.toLowerCase() == 'a' || (event.target = event.target.getParent('a'))) && event.target.href.contains('grid')) {
				event.preventDefault();
				event.target.blur();
				gallery.changeMode('#' + event.target.href.split('#')[1]);
			}
		})
	},
	
	build: function() {
		var gallery = this._gallery;

		this._gallery.getAlbumList(function(albumList) {
	
			albumList.forEach(function(photoset_id, i) {
			
				gallery.getAlbumData(photoset_id, function(album) {

					var albumDiv = new elementFromTemplate('template_album', {
						album_title: album.title,
						album_class: (i % 2) ? 'even' : '',
						album_description: album.description,
						album_date: '',
						album_linkHref: '#/grid/' + photoset_id,
						album_numberOfPhotos: album.numberOfPhotos,
						album_keyImageSrc: album.keyPhotoSrc
					}).inject('albums');
	
					// Add album date - requires a separate API call; this is a bit of a hack.
					FlickrAPI.get({method: 'flickr.photos.getInfo', photo_id: album.keyPhotoID}, function(data) {
	
						var monthName = ["January","February","March","April","May","June",
										 "July","August","September","October","November","December"];
						// Formats the date to 'Month yyyy'
						albumDiv.getElement("h4").set('text', monthName[data.photo.dates.taken.slice(5, 7) - 1] + " " + data.photo.dates.taken.slice(0, 4));	
					});
				});
			});
		});
			
		this.built = true;
	}
});

var Grid = new Class({

	initialize: function(gallery) {
		this._gallery = gallery;
		this._currentAlbum = '';

		this._preferences = new Hash.Cookie('PhotoGalleryPreferences', {duration: 365, path: '/'});
		this._preferences.set('thumbsSize', this._preferences.get('thumbsSize') || 80);

		gallery.addEvent('modeChange', function(mode, photoset_id) {
			$$('.thumbnails').setStyle('display', 'none');
			if (mode != 'grid') return;
			if (mode == 'grid' && !$('grid' + photoset_id))
				this.build(photoset_id);
			else
				$('grid' + photoset_id).erase('style');
			this._currentAlbum = photoset_id;
			this._gallery.getAlbumData(photoset_id, function(album) {
				document.title = album.title;
				$('albumTitle').set('text', album.title);
			});
			if (!this.sliderBuilt)
				this._createSlider();
		}.bind(this));
	},
	
	build: function(photoset_id) {
		var galleryDiv = new Element('div', {
			id: 'grid' + photoset_id,
			'class': 'grid thumbnails',
			events: {
				click: function(event) {
					new Event(event);
					if ((event.target.nodeName.toLowerCase() == 'a' || (event.target = event.target.getParent('a'))) && event.target.href.contains('single')) {
						event.preventDefault();
						event.target.blur();
						gallery.changeMode('#' + event.target.href.split('#')[1]);
					}
				}
			}
		}).inject('photos');
		
		var gallery = this._gallery;
		
		this._gallery.getAlbumPhotos(photoset_id, function(photos){
		
			var photo = photos['photo' + photos._firstImage];
		
			do {
				var thumbnail = new elementFromTemplate('template_thumbnail', {
					thumbnail_linkHref: '#/single/' + photoset_id + '/' + photo.id,
					thumbnail_title: photo.title,
					thumbnail_src: photo.srcBase + "_m.jpg"
				}).inject(galleryDiv);

				thumbnail.getElement('img').addEvent('load', function() {
					if (this.width > this.height)
						this.addClass('landscape');
					else
						this.addClass('portrait');
					this.removeEvent('load', arguments.callee);			
				});
			} while (photo = photos['photo' + photo.next]);
		});
	},
	
	getThumbnailBorders: function() {
		var border = document.getElement('div.thumbnail .imageBorder');
		if (!border) return {};
		var img = document.getElement('div.thumbnail img');
		var borders = {
			x: border.getSize().x - img.getSize().x,
			y: border.getSize().y - img.getSize().y
		}
		this.getThubmnailBorders = function() {
			return borders;
		}
		return borders;
	},
	
	resizeThumbnails: function(size) {
	
		var border = this.getThumbnailBorders();
	
		var css = new Stylesheet('photos.css');
		css.getRule('.thumbnail').style.width = size + 'px';
		css.getRule('.thumbnail').style.height = size + 'px';
		css.getRule('.landscape').style.width = size - (border.x || 10) + 'px';
		css.getRule('.portrait').style.height = size - (border.y || 10) + 'px';	
	},

	_createSlider: function() {
	
		var grid = this;
			
		new Slider($('resizeSlider'), $('resizeHandle'), {
			steps: 160,	
			onChange: function(pos){
				grid.resizeThumbnails(pos + 80);
			},
			onComplete: function(pos){
				grid._preferences.set('thumbsSize', pos);			
			}		 
		}).set(this._preferences.get('thumbsSize').toInt());
		
		this.sliderBuilt = true;
	}
});

var DisplayPhoto = new Class({
	
	Implements: Events,
	
	initialize: function(gallery) {
		this._gallery = gallery;
		this._displayPhoto = document.getElement('#displayPhoto .imageBorder');
		this._currentPhoto = '';
		this._currentAlbum = '';
		this._fadeout = new Fx.Tween(this._displayPhoto, {property: 'opacity', duration:400, transition: Fx.Transitions.Sine.easeInOut});
		this._fadein = new Fx.Tween(this._displayPhoto, {property: 'opacity', duration:300, transition: Fx.Transitions.Sine.easeInOut});
		
		var css = new Stylesheet('photos.css').getRule('#displayPhoto');
		this._margin = {
			top: parseInt(css.style.marginTop),
			right: parseInt(css.style.marginRight),
			bottom: parseInt(css.style.marginBottom),
			left: parseInt(css.style.marginLeft)
		}
		css.style.margin = '0';
		
		var displayPhoto = this;
		
		$('nextPhoto').addEvent('click', function(event) {
			new Event(event).preventDefault();
			this.blur();
			displayPhoto.next();
		});
		
		$('previousPhoto').addEvent('click', function(event) {
			new Event(event).preventDefault();
			this.blur();
			displayPhoto.previous();
		});
		
		gallery.addEvent('modeChange', function(mode, photoset_id, image_id) {
			if (mode != 'single' && mode != 'filmstrip') this.exit();
			else this.enter(photoset_id, image_id);
		}.bind(this));
		
		//Setup keyboard shortcuts
		
		document.addEvent('keydown', function(event) {
			this.keyPressed.call(this, event);
		}.bind(this));

		this.exit();
	},

	enter: function(photoset_id, image_id) {
	
		this.findDisplayArea();
		
		if (this._active) return this.resizeDisplayImage();
	
		var displayPhoto = this;
		
		function onResize() {
			displayPhoto._area = displayPhoto.findDisplayArea();
			displayPhoto.resizeDisplayImage();
		}

		window.addEvent('resize', onResize);
		
		this.addEvent('exit', function(){
			window.removeEvent('resize', onResize);
			this.removeEvent('exit', arguments.callee);
		});

		this._active = true;
		
		this._gallery.getAlbumData(photoset_id, function(album) {
			$('albumTitle').set('text', album.title);
		});
		
		this.show(photoset_id, image_id);
	},
	
	exit: function() {
		this._displayPhoto.setStyle('opacity', 0);
		this._active = false;
		this._currentPhoto = '';
		this._currentAlbum = '';
		$('loading').style.display = 'none';
		this.fireEvent('exit');
	},
	
	keyPressed: function(event) {
		if (this._gallery.mode != 'single' && this._gallery.mode != 'filmstrip') return;
		event = new Event(event);
		switch (event.key) {
			case 'up':
			case 'left':
			case 'j':
				return this.previous();
			case 'down':
			case 'right':
			case 'k':
				return this.next();
		}
	},
	
	getVariant: function(photoset_id, image_id, callbackFunction) {
		this._gallery.getSizes(photoset_id, image_id, function(variants) {
			var area = this._area;
			var criticalDimension = variants.ratio * area.height > area.width ? 'width' : 'height';		

			var i;
			if (noResize) {
				for (i = variants.length - 1; i > 0; i--)
					if (variants[i][criticalDimension] <= area[criticalDimension])
						break;
			}
			else {
				for (i = 0; i < variants.length - 1; i++)
					if (variants[i][criticalDimension] >= area[criticalDimension])
						break;
			}
			
			// Don't load massive images.		
			if (variants[i].label == 'Original' && (variants[i].width > 1024 || variants[i].height > 1024))
				i--;

			callbackFunction(variants[i].src);
		}.bind(this));
	},

	findDisplayArea: function(){
		var upperBound = this._margin.top + $('header').getCoordinates().bottom;
		var lowerBound = Math.min($('controlBar').getCoordinates().top, $('displayTitle').getCoordinates().top);

		return this._area = {
			width: window.getWidth()
			 	- (this._margin.left + this._margin.right)
				- ($('displayPhoto').getSize().x - $('displayPhoto').getElement('img').getSize().x),
			height: lowerBound
				  - upperBound
				  - ($('displayPhoto').getSize().y - $('displayPhoto').getElement('img').getSize().y)
				  - this._margin.bottom,
			offsetTop: upperBound
		}
	},

	resizeDisplayImage: function() {
	
		// Check there's a photo to resize
		if (!this._currentPhoto) return;

		var area = this._area;
		var img = $('displayPhoto').getElement('img');

		this._gallery.getSizes(this._currentAlbum, this._currentPhoto, function(imageData)  {
			img.style.width = Math.min(area.width, area.height / imageData.ratio, img.naturalWidth) + 'px';
			img.style.height = (img.style.width.toFloat() * imageData.ratio) + 'px';
			$('displayPhoto').style.top = Math.max(area.offsetTop, area.offsetTop + ((area.height - $('displayPhoto').getSize().y) / 2)) + 'px';
		});
	},
	
	show: function(photoset_id, image_id) {
		if (this._currentPhoto == image_id) return;

		this._loading = true;
		
		var loading = $('loading');
		
		var displayPhoto = this;
		
		this.getVariant(photoset_id, image_id, function(src) {
			new Asset.image(src, {onload: switchPhotos});
			loading.style.display = 'block';
		}.bind(this));
		
		function switchPhotos() {

			displayPhoto._gallery.getImageData(photoset_id, image_id, function(imageData){
				
				// In the following code, this == img
				displayPhoto._fadeout.start(0).chain(function(){

					displayPhoto._gallery.getAlbumData(photoset_id, function(album) {
						document.title = album.title + (imageData.title.trim() ? ': ' + imageData.title : '');
					});
					
					// Insert image
					this.replaces($('displayPhoto').getElement('img'));
					displayPhoto._currentAlbum = photoset_id;
					displayPhoto._currentPhoto = image_id;

					// Store default width
					if (!this.naturalWidth) {
						this.naturalWidth = this.width;
					}

					// Display title and description
					$('photoTitle').set('text', this.title = imageData.title);
					$('photoDescription').set('text', this.alt = imageData.description);
	
					// Resize and position
					displayPhoto.findDisplayArea();
					displayPhoto.resizeDisplayImage();
	
					// Setup download links
					$$('#download a').each(function(link) {
						switch (link.id) {
							case 'downloadSmall':
								link.href = imageData.srcBase + '_d.jpg';
								break;
							case 'downloadLarge':
								link.href = imageData.srcBase + '_b_d.jpg';
								break;
							case 'downloadOriginal':
								link.href = imageData.originalSrc;
								link.style.display = imageData.originalSrc.test(/undefined/) ? 'none' : '';
								break;
						}
					});

					loading.style.display = 'none';
					displayPhoto._gallery.fireEvent('photoChange', [photoset_id, image_id]);
					displayPhoto._fadein.start(1);
					displayPhoto._gallery.history.addState(displayPhoto._gallery.mode + '/' + photoset_id + '/' + image_id);
					displayPhoto._loading = false;

				}.bind(this));
				
				// Preload next/prev image.
				displayPhoto._gallery.getContext(photoset_id, image_id, function(context){
					if (context.next) {
						$('nextPhoto').style.visibility = 'visible';
						displayPhoto.getVariant(photoset_id, context.next, function(src) {
							new Asset.image(src);
						});
					}
					else
						$('nextPhoto').style.visibility = 'hidden';

					if (context.previous) {
						$('previousPhoto').style.visibility = 'visible';
						displayPhoto.getVariant(photoset_id, context.previous, function(src) {
							new Asset.image(src);
						});
					}
					else
						$('previousPhoto').style.visibility = 'hidden';
				});
			}.bind(this));
		}
	},
	
	next: function() {
		if (this._loading)
			return;
		this.fireEvent('next');
		this._gallery.getContext(this._currentAlbum, this._currentPhoto, function(context) {
			if (!context.next)
				return this._gallery.changeMode('grid', this._currentAlbum);
			this.show(this._currentAlbum, context.next);
		}.bind(this));
	},
	
	previous: function() {
		if (this._loading)
			return;
		this.fireEvent('previous');
		this._gallery.getContext(this._currentAlbum, this._currentPhoto, function(context) {
			if (!context.previous)
				this._gallery.changeMode('grid', this._currentAlbum);
			this.show(this._currentAlbum, context.previous);
		}.bind(this));
	}
});

var Filmstrip = new Class({

	initialize: function(gallery) {
		this._gallery = gallery;
		this._visible = false;
		this._strip = $('strip');
		a = this._scroll = new Fx.Scroll('strip', {
			duration: 750,
			transition: Fx.Transitions.Quad.easeInOut
		});

		gallery.addEvent('photoChange', function(photoset_id, image_id) {
			this.updateSelected(photoset_id, image_id);
		}.bind(this));
		gallery.addEvent('modeChange', function(mode, photoset_id) {
			if (mode != 'filmstrip') return;
			$$('div[id^=stripContainer]').setStyle('display', 'none');
			if (!$('stripContainer' + photoset_id))
				this.build(photoset_id);
			else
				$('stripContainer' + photoset_id).setStyle('display', 'block');
		}.bind(this));
		
		this._strip.addEvent('click', function(event) {
			new Event(event);
			if (event.target.nodeName.toLowerCase() == 'a' || (event.target = event.target.getParent('a'))) {
				event.target.blur();
				event.preventDefault();
				var state = event.target.href.split('#')[1].split('/')
				gallery.displayPhoto.show(state[2], state[3]);
			}
		});
	},

	build: function(photoset_id) {
		var galleryDiv = new Element('div', {
			id: 'stripContainer' + photoset_id
		});
		
		var gallery = this._gallery;
		
		this._gallery.getAlbumPhotos(photoset_id, function(photos){

			var photo = photos['photo' + photos._firstImage];
			do {
				new elementFromTemplate('template_stripThumbnail', {
					thumbnail_title: photo.title,
					thumbnail_src: photo.srcBase + "_m.jpg",
					thumbnail_id: 'stripThumbnail' + photoset_id + photo.id,
					thumbnail_href: '#/filmstrip/' + photoset_id + '/' + photo.id
				}).inject(galleryDiv);
			} while (photo = photos['photo' + photo.next]);
			
			galleryDiv.inject('strip');

			// Resize display image.
			gallery.displayPhoto.findDisplayArea();
			gallery.displayPhoto.resizeDisplayImage();
		});
	},

	updateSelected: function(photoset_id, image_id) {
		if (!$('stripThumbnail' + photoset_id + image_id))
			return;
		if (this._currentlySelected)
			this._currentlySelected.removeClass('selected');
		this._currentlySelected = $('stripThumbnail' + photoset_id + image_id).addClass('selected');

		var scrollX = this._currentlySelected.offsetLeft + (this._currentlySelected.getSize().x / 2) - (this._strip.getSize().x / 2);

		//console.log(scrollX);
		this._scroll.start(scrollX, false);
	}

});