// set up an overlay object
var overlay = new BlipOverlay(),

actions={

	getSettingData:function(sel){
		return  $.map($(sel+' input[type=checkbox]'), function(i){
			return i.id+'='+(i.checked ? 1 : 0);
			}).join('&');
		},

	/*
	The dispatch() method allows us to funnel event handlers to this single function, 
	rather than creating multiple handlers for different functions.
	*/
	dispatch:function(e){
		var obj=actions[e.type];
		if (obj){
			var id=e.target ? e.target.id : null, key, m, k;
			if (id){
				if (obj[id]){
					key=id;
					}
				else{
					for (k in obj){
						if (m = id.match(new RegExp(k))){
							key=k;
							break;
							}
						}
					}
				if (key && obj[key]){	// call the function passing the event and any regex match.
					return obj[key].call(this, e, m);
					}
				}
			}
		},
	click:{
		'show_comm':function(e){
		
			if (!blipData.commentState){

				/* If comments not already loaded, get them via ajax. */		
				if (!blipData.commentsLoaded){
	
					/*  We give a 'loading comments' message, but only after
					the comments have taken > 0.5 seconds to load. */
					var link=$(e.target), count = null, timer = setTimeout(function(){
						count = link.html();
						link.html('loading comments...');
						}, 500);
	
					$.post(blipData.root+'internal/ajax/entry/comments','id='+blipData.id,function(json){
						var list = json.comments,
						unread_ids = json.unread_ids;
						
						if (list){						
							var html=[], i=0, l=list.length, c, j, k;
							for (;i<l;i++){
								c=list[i];								
								html.push("<li"+($.inArray(c.id, unread_ids)>=0 ? " class='new'" : '')+"><p>");
								html.push(c.text);
								html.push("</p><p"+(c.is_owner ? " class='author'" : "")+"> ~ <a href='"+blipData.root+c.commentor_name+"' class='commentor'>"+c.commentor_name+"</a> "+c.icons.join(' ')+"</p><span id='comment_edit'>");
								if (blipData.owner && blipData.viewer!=c.commentor_name && !c.children){
									html.push("<a class='reply' href='#reply"+c.id+"' id='reply"+c.id+"'>Reply</a>");
									}
								if (blipData.editComments || blipData.viewer==c.commentor_name){
									html.push("<a class='edit' href='/comment/"+c.id+"/edit'>Edit</a>");
									if (!c.children){		// cant delete comments when they have children
										html.push("<a class='del' href='/comment/"+c.id+"/delete'>Delete</a>");
										}
									}
								else if (blipData.owner && !c.children){
									html.push("<a class='del' href='/comment/"+c.id+"/delete'>Delete</a>");
									}
								html.push("</span></li>");
								
								if (c.children){
									j = 0, k = c.children.length;
									html.push("<li class='parent'><ol class='comment-child-list' id='comment_list_"+c.id+"'>");
									for (;j<k;j++){
										ch = c.children[j];
										html.push("<li"+($.inArray(ch.id, unread_ids)>=0 ? " class='new'" : '')+"><p>");
										html.push(ch.text);
										html.push("</p><p"+(ch.is_owner ? " class='author'" : "")+"> ~ <a href='"+blipData.root+ch.commentor_name+"' class='commentor'>"+ch.commentor_name+"</a> "+ch.icons.join(' ')+"</p><span id='comment_edit'>");
										if ((blipData.owner || blipData.viewer==c.commentor_name) && j==c.children.length-1){
											html.push("<a class='reply' href='#reply"+c.id+"' id='reply"+c.id+"'>Reply</a>");
											}
										if (blipData.editComments || blipData.viewer==ch.commentor_name){
											html.push("<a class='edit' href='/comment/"+ch.id+"/edit'>Edit</a><a class='del' href='/comment/"+ch.id+"/delete'>Delete</a>");
											}
										else if (blipData.owner){
											html.push("<a class='del' href='/comment/"+ch.id+"/delete'>Delete</a>");
											}
										html.push("</span></li>");
										}
									html.push("</ol></li>");
									}
								}
							$('#comment_list').html(html.join(''));
							}
						clearTimeout(timer);
						timer=null;
						if (count){
							link.html(count);
							}
						$(window).scrollTop($('#comments').show().offset().top);	// show the comments, and force the browser to scroll to the top of the comment list
						blipData.commentsLoaded=1;
						}, 'json');
					}
				else {
					$(window).scrollTop($('#comments').show().offset().top);
					}
				blipData.commentState=1;
				}
			else {
				$('#comments').hide();
				blipData.commentState=0;
				}
			
			return false;
			},

		'show_(tags|groups)':function(e, m){
			var b = '#'+m[1]+'_box', v=$(b).toggle().is(':visible');
			$(e.target).html((v ? 'hide' : 'show')+' '+m[1]);
			if (v){
				$(window).scrollTop($(b).offset().top);
				}
			return false;
			},
			
		/* ratings */
		
		'^star_(\\d)':function(e, m){
			var r = m[1]*1;
			
			$.post(blipData.root+'internal/ajax/entry/rate', 'id='+blipData.id+'&rating='+r);
			
			// reset the rating display
			$('#rating a').each(function(i, n){
				n.id=null;
				n.className='star '+(i >= r ? 'down' : 'up');
				n.title='You rated this entry '+r+' (currently scored '+(blipData.rating+r)+')';
				});
			delete actions.click['^star_(\\d)'];
			
			return false;
			},
			
		/* subscriptions */
		
		'^toolbar_(un)?subscribe':function(e, m){

			var id=e.target.id, sel = '#'+id.replace('toolbar','overlay'), sub=!m[1];
			
			// post the un/subscription
			$.post(blipData.root+'internal/ajax/journal/subscriptions', 'journal='+blipData.username+'&state='+(sub ? '1' : '0'));
			
			// show overlay and toggle button state
			overlay.show(sel, function(){
				setTimeout(function(){
					overlay.hide(sel);
					$('#'+id).attr('id',sub ? 'toolbar_unsubscribe' : 'toolbar_subscribe').html((sub ? 'unsubscribe from' : 'subscribe to')+' this journal');
					}, blipData.delay);
				});
			return false;
			},
		
		/* blipcards */
		
		'^toolbar_printondemand':function(e, m){
			var id = e.target.id, sel = '#overlay_printondemand';
			$.post('/internal/ajax/entry/printondemand','image_id='+blipData.id,function(r){
				$(sel+' ul').html(r);
				});			
			
			overlay.show(sel);
			
			$(sel+' input').bind('click',function(e){
				overlay.hide(sel);
				$(sel+' input').unbind('click');
				});
			
			return false;
			},
			
		/* favourites */
		
		'toolbar_favourite':function(e){
			var sel='#overlay_favourite';
			overlay.show(sel);
			$(sel+' input').bind('click',function(e){
			
				if ($(this).hasClass('cancel')){
					overlay.hide(sel);
					}
				else{
					$.post(blipData.root+'internal/ajax/entry/favourite', 'id='+blipData.id);
					var outer = $(sel+' div.outer'), success = $(sel+' div.success');
					outer.slideUp(500, function(){
						success.show();
						setTimeout(function(){
							overlay.hide(sel);
							$('#toolbar_favourite').parent().remove();
							},blipData.delay);
						});
					}
				
				$(sel+' input').unbind('click');
				});
			return false;
			},
		'toolbar_nofavourite':function(e){
			var sel='#overlay_nofavourite';
			overlay.show(sel);
			$(sel+' input').bind('click',function(e){
				overlay.hide(sel);
				$(sel+' input').unbind('click');
				});
			return false;
			},
			
		/* large image */
			
		'toolbar_zoom':function(e){
			$(e.target).addClass('loading');
			var preloader = $('zoom_preloader');
			if (!preloader.length){
				preloader = $("<iframe class='preloader'><"+"/iframe>").appendTo(document.body);
				}
			preloader.attr('src','/internal/preloadimage?callback=window.top.actions.callback.toolbar_zoom&img='+encodeURIComponent(blipData.zoomUrl));
			e.preventDefault();
			e.stopPropagation();
			return false;
			},
		'overlay_zoom_image':function(e){
		
			overlay.hide(null, function(){
				$('#overlay_zoom').hide();
				$('#overlay').removeClass('zoom');
				overlay.duration=500;
				});

			e.preventDefault();
			e.stopPropagation();
			return false;
			},
			
		/* share */
		
		'toolbar_share':function(e){
			var sel = '#overlay_share';
			overlay.show(sel);
			$(sel+' p.submit input').bind('click',function(e){
				if ($(this).hasClass('cancel')){
					overlay.hide(sel);
					}
				else{
					$.post(blipData.root+'internal/ajax/entry/share', 'id='+blipData.id+'&recipients='+encodeURIComponent($('#share_recipients').val())+'&message='+encodeURIComponent($('#share_message').val()));
					var outer = $(sel+' div.outer'), success = $(sel+' div.success');
					outer.slideUp(500, function(){
						success.show();
						setTimeout(function(){
							overlay.hide(sel, function(){
								outer.show();
								success.hide();
								$('#share_recipients').val('');
								$('#share_message').val('I saw this on Blipfoto and thought you might like it:');
								});
							},blipData.delay);
						});
					}
				
				$(sel+' input').unbind('click');
				});
			return false;
			},
		
		/* groups & settings - these are very similar so this handles both */
		
		'toolbar_(groups|settings)':function(e, m){
		
			var type=m[1],
			sel='#overlay_'+type,
			me = arguments.callee;						// reference this function,
			submitFn = actions.submit['toolbar_(groups|settings)'];	// reference the function that will be called when "save" is clicked
			
			me.curType=type;
			if (!me.cache){				// create the cache.
				me.cache={};
				}
			if (type=='settings' && !me.cache[type]){	//  If settings were requested, these already exist in the DOM so create the cache object now (the settings are stored in the blip_image table and are therefore already fetched, so we may as well output them in the DOM even if they're not going to be used, in order to save an ajax call).
				me.cache.settings=actions.getSettingData(sel);
				}
			
			if (me.cache[type]){						// if the type's cache object exists, we know the HTML exists so we can just go ahead and show the overlay.
				overlay.show(sel);
				$(sel+' input.bttn').bind('click', submitFn);
				}
			else{										// otherwise, we need to fetch the entry's group list via ajax, then build the HTML once both the POST response is returned and the overlay fade is done.
			
				var json=false, t;
				$.post('/internal/ajax/entry/'+type,'id='+blipData.id+'&mode=list_status', function(r){
					json = $.parseJSON(r);
					});
				overlay.show(null, function(){
					if (json!==false){
						clearTimeout(t);
						
						if (json===null){
							return;
							}
						
						var html=[], i=0, l=json.length, s, data=[], key=type=='groups' ? 'group' : 'setting', id;

						// if this is groups but they user doesn't belong to any groups yet, we need to handle this differently.
						
						if (type=='groups' && !l){
							$(sel+' h3').html("<a href='/groups'>Groups</a> are places on Blipfoto where you can find people and pictures based on shared interests. Once you've joined some groups, you'll be able to add entries to them here.");
							$(sel+' input.bttn').val('OK');
							}
						else {
							for (; i<l; i++){
								s = json[i];
								id=key+'_'+s[key+'_id'];
								data.push(id+'='+(s.selected ? 1 : 0));
								html.push('<li><input type="checkbox" id="'+id+'"'+(s.selected ? ' checked="checked"' : '')+(s.rejected ? ' disabled="disabled"' : '')+' /><label for="'+id+'">'+s.name+'</label></li>');
								}
							me.cache[type]=data.join('&');
							$(sel+' ul').html(html.join(''));
							}
						$(sel+' input.bttn').bind('click', submitFn);
						$(sel).show();
						t = json = null;
						}
					else {
						t = setTimeout(arguments.callee, 50);
						}
					});
				}
			return false;
			},
			
		/* calendar */
			
		'cal_(next|prev)':function(e, m){
		
			var cal = blipData.calendar,
			ol = $('div.calendar ol');
		
			// create the cache if needed
			if (!cal.cache){
				cal.cache={
					obj:{},
					add:function(html){
						this.obj[cal.current.m+''+cal.current.y]=html;
						return html;
						},
					get:function(m, y){
						return this.obj[m+''+y] || 0;
						}
					};
				cal.cache.add(ol.html());		// since this is the first refresh, add the existing HTML to the cache
				}
			
			// adjust the month / year
			
			var m = cal.current.m + (m[1]=='prev' ? -1 : 1), 
			y = cal.current.y;
			
			if (!m){
				m=12;
				y--;
				}
			else if (m > 12){
				m=1;
				y++;
				}
				
			$('#cal_next')[m < cal.max.m || y < cal.max.y ? 'show' : 'hide']();
			$('#cal_title').html(cal.mn[m-1]+' '+y);
			
			cal.current.m=m;
			cal.current.y=y;
			
			// check the cache, and if nothing found issue a POST request
		
			var html = cal.cache.get(m, y);
			
			if (!html){
				$.post(blipData.root+'internal/ajax/entry/calendar', 'u='+blipData.username+'&m='+m+'&y='+y, function(r){
				
					r = $.parseJSON(r);
					
					var tmp=[], days=[], i=1, d;
					for (; i < r.weekday_start; i++){				// calendar starts on monday, so add empty days to pad to the start of the month
						tmp.push('<li><span></span></li>');
						}
					for (x in r.days){								// r.days is object with numeric keys, so reorder into indexed array
						days[x*1]=r.days[x];
						}
					for (i=1;i<days.length;i++){					// output
						d = days[i];
						tmp.push('<li'+((!((i+(r.weekday_start-1)) % 7)) ? ' class="last"' : '')+'>');
						if (d){
							if (d.id==blipData.id){
								tmp.push('<time class="active">'+i+'</time>');
								}
							else{
								tmp.push('<a href="/entry/'+d.id+'"><time>'+i+'</time></a>');
								}
							}
						else{
							tmp.push('<span>'+i+'</span>');
							}
						tmp.push('</li>');
						}
					
					html = cal.cache.add(tmp.join(''));				// add to cache, and store in html variable. The timeout below will display this on the next loop.
					});
				}
			
			// fade out the existing list immediately; if the POST request is ongoing, this will at least give a 'working' indication.
			
			ol.fadeOut('fast', function(){
				if (html){											// when HTML available (via cache or POST) display it
					ol.html(html).fadeIn('fast');
					html=null;
					}
				else{
					setTimeout(arguments.callee, 50);
					}
				});
			
			e.preventDefault();
			e.stopPropagation();
			return false;
			},
			
		'reply(\\d+)':function(e, m){
		
			var rf = blipData.replyForm,
			bb = true,
			cid = m[1];
			
			// if the IDs match, the comment form is already open so just close it.
			if (blipData.replyToComment==m[1]){
				blipData.replyForm.slideUp(200, function(){
					blipData.replyForm.remove();
					});
				blipData.replyToComment = -1;
				}
			
			// otherwise it needs to be opened.
			else {
			
				// if not already done, clone the comment form (while updating IDs and removing unused elements)
				if (!rf){
				
					var rf = $('<li id="reply"></li>').append($('#editbox').clone().attr('id','reply_editbox'));
					
					rf.find('h3').html('Add reply');
					rf.find('#comment_form').attr('id', 'reply_comment_form');
					rf.find('#bb_buttons').attr('id', 'reply_bb_buttons');
					rf.find('#comment_text').attr('id', 'reply_comment_text');
					rf.find('#submit_comment').attr('id', 'reply_submit_comment');
					rf.find('#track_comment, #track_status').remove();
						
					// attach submit listener
					$('#reply_comment_form').live("submit", actions.dispatch);
					
					blipData.replyForm = rf;
					
					bb = false;
					}
				
				// store the ID of the comment we're replying to
				blipData.replyToComment = m[1];
					
				// attach form and display
				$(e.target.parentNode.parentNode).after(rf);
				rf.slideDown(200);
				
				// attach reply field and set the parent_id (for non-ajax submission IE < 9)
				var rpid = $('#reply_parent_id');
				if (!rpid.length){
					rpid = $('<input type="hidden" name="parent_id" id="reply_parent_id" value="" />');
					$('#reply_comment_form').append(rpid);
					}
				rpid.val(blipData.replyToComment);
				
				if (!bb){
					// hook up BBCode
					var replyEditor = new BlipBB($('#reply_comment_text')[0]);
					$('#reply_bb_buttons input').click(function(e){
						var bb=e.target.className.match(/blipbb_(\w+)/)[1];
						if (bb=='link'){
							replyEditor.insert_url();
							}
						else{
							replyEditor.insert(bb);
							}
						});
					}
				}
			
			e.preventDefault();
			e.stopPropagation();
			return false;
			}
			
		},
	mouseover:{
		'^toolbar_.+$':function(e){
			$('#toolbar_label').html(e.target.innerHTML);
			},
		'^star_(\\d)$':function(e, m){
			var r = m[1]*1;
			$('#rating a').each(function(i, n){
				n.className='star '+(i < r ? 'up' : '');
				});
			}
		},
	mouseout:{
		'^toolbar_.+$':function(e){
			$('#toolbar_label').html("");
			},
		'^star_\\d$':function(e){
			$('#rating a').each(function(i, n){
				n.className='star';
				});
			}
		},
	submit:{
		'(reply_)?comment_form':function(e, m){
			
			var prefix = m[1] || '';
		
			// get the comment text
			var comment = $('#'+prefix+'comment_text').val();
			
			// check if the comment text is empty, or the same as the last comment (if it is, the user probably didn't mean to submit twice.)
			if (comment == blipData.lastComment){
				return false;
				}
				
			// disable the submit button and update its message, so the user can't re-submit while waiting for the POST request
			$('#'+prefix+'submit_comment').attr('disabled','disabled').val('submitting...');
			
			// store the last comment that was added, so we can compare it next time.
			blipData.lastComment = comment;
			
			// get the rest of the form data
			var frm=$(e.target), data={
				'entry_id':frm.find('input[name="entry_id"]').val(),
				'parent_id':prefix ? blipData.replyToComment : '0',
				'comment':comment,
				'track_comment':$('#track_comment:checked').length ? 1 : 0,
				'json':'1'
				};
			
			// append the '.json' identifier to the form's action and POST.
			$.post(frm.attr('action')+'.json', data, actions.callback.comment_complete, 'json');
			return false;
			},
				
				
		'toolbar_(groups|settings)':function(){
		
			// get the data cache for the setting type
			var fn = actions.click['toolbar_(groups|settings)'],
			type=fn.curType,
			sel = '#overlay_'+type,
			cache = fn.cache[type],
			refresh = 0, 
			data = actions.getSettingData(sel);
		
			// detatch handler
			$(sel+' input.bttn').unbind('click');
			
			if (data){
				
				if (data!=cache){		// if changed, POST the new data now
					$.post('/internal/ajax/entry/'+type, 'id='+blipData.id+'&mode=update&'+data);
					refresh=1;
					}
					
				fn.cache[type]=data;
				
				var outer = $(sel+' div.outer'), success = $(sel+' div.success');
				outer.slideUp(500, function(){
					success.show();
					setTimeout(function(){
						overlay.hide(sel, function(){
							outer.show();
							success.hide();
							if (refresh){
								history.go(0);
								}
							});
						},blipData.delay);
					});
				}
			else {
				// there's no data to send, so just close.
				overlay.hide(sel);
				}
			}
		},
	callback:{
		'toolbar_zoom':function(h){
			$('#overlay').addClass('zoom');
			$('#overlay_zoom img').attr('src',blipData.zoomUrl);
			$('#toolbar_zoom').removeClass('loading');
			overlay.duration=1300;
			overlay.show('#overlay_zoom', null, h);
			},
		'comment_complete':function(json){
			
			if (json && json.comment_id){
				
				// remove the 'last comment' flags
				$('#lc').removeAttr('id');
				$('li.last').removeClass('last');
				
				var parent, 
				wrapper=0,
				replyButton,
				li,
				link;

				// is it a reply?
				if (json.parent_id){
					
					// if it is, determine whether or not its the first reply. If so, we need to create a wrapper <ol> as the parent
					parent = $('#comment_list_'+json.parent_id);
					if (!parent.length){
						parent = $("<ol class='comment-child-list' id='comment_list_"+json.parent_id+"'></ol>");
						wrapper = 1;
						}
					}
				else {
					parent = $('#comment_list');
					}
				
				parent.append(["<li id='lc' class='last new'><p>", json.comment, "</p><p> ~ <a href='/"+json.commentor.username+"' class='commentor'>"+json.commentor.username+"</a> "+json.commentor.icons.join(' ')+"</p><span id='comment_edit'>"+(json.parent_id ? "<a class='reply' href='#reply"+json.parent_id+"' id='reply"+json.parent_id+"'>Reply</a>" : "")+"<a class='edit' href='/comment/"+json.comment_id+"/edit'>Edit</a><a class='del' href='/comment/"+json.comment_id+"/delete'>Delete</a></span></li>"].join(''));

				// is it a reply?
				if (json.parent_id){
					replyButton = $('#reply'+json.parent_id);
					if (wrapper){
						// if we created a wrapper <ol>, it needs to be inserted after the comment we're replying to
						li = replyButton.closest('li');
						
						// we also need to remove the 'delete' icon (as can't delete comments which have children)
						li.find('a.del').remove();
						li.after($("<li class='parent'></li>").append(parent));
						}
						
					// now remove the original reply button
					replyButton.remove();
					}
				
				// increment the comment count
				link=$('#show_comments'), count = parseInt(link.html().match(/^(\d+)/)[1])+1;
				link.html(count+' comment'+(count==1 ? '' : 's'));
				
				// get rid of the "no comments" message if it exists
				$("#no_comments").remove();
				}
			if (json && json.tracking_updated){
				var html = $('#track_status').html();
				$('#track_status').html(html+' (updated)');
				setTimeout(function(){
					$('#track_status').html(html);
					}, 2000);
				}
				
			var prefix = json.parent_id ? 'reply_' : '';
				
			// reset the textarea and submit button
			$('#'+prefix+'comment_text').val('');
			$('#'+prefix+'submit_comment').removeAttr('disabled').val('Submit');
			
			// remove the comment form
			if (json.parent_id){
				blipData.replyForm.css({display:'none'}).remove();
				blipData.replyToComment=0;
				}
			
			}
		}
	};

// attach general click listener
$('#entry').live("click",actions.dispatch);

// listener for comment form submission
$('#comment_form').live("submit", actions.dispatch);

// listeners for toolbar hover (desktop only)
$('div.desktop #entry_image').mouseenter(function(e){
	$('#entry_toolbar').animate({'height':25}, 400);
	}).mouseleave(function(e){
	$('#entry_toolbar').animate({'height':0}, 400);
	});
	
// listener for closing image zoom
$('#overlay_zoom_image').click(actions.dispatch);

// live listener for #large in entry text
$('a[href$=large]').live('click',actions.click.toolbar_zoom);

// listeners for toolbar items
$('#entry_toolbar li a').mouseover(actions.dispatch).mouseout(actions.dispatch);

// hook up BBCode
var commentEditor = new BlipBB($('#comment_text')[0]);
$('#bb_buttons input').click(function(e){
	var bb=e.target.className.match(/blipbb_(\w+)/)[1];
	if (bb=='link'){
		commentEditor.insert_url();
		}
	else{
		commentEditor.insert(bb);
		}
	});
	
/**** STREAM NAVIGATION ****/

var StreamQ = {
	
	init:function(){
			
		// set the type list
		var x, types=['upload'];
		if (blipData.isVisibleByDate){
			types.unshift('date');
			}
		if (blipData.isSubscribed){
			types.push('subscribed');
			}

		// determine the current type
		this.currentType = x = blipData.streamCurrent;
		
		if ($.inArray(x, types) < 0){
			types.push(x);
			}
		
		// set the nav button's href hash to the current stream
		$('#streamnav').attr('href','#'+x);
		
		// add the type list to the DOM.
		var c = types.length-1, ct = this.currentType;
		$('#stream ul').html($.makeArray($(types).map(function(i, t){
			return '<li><a href="#'+t+'"'+(t==ct ? ' class="active"' : '')+'>'+t+'</a>'+(i < c ? ' : ' : '')+'</li>';
			})).join(''));
			
		},
	
	// find the entries within the queue for the given / default type
	find:function(type){
		type = type || this.currentType;
		var id = blipData.id, 
		q = blipData.stream[type], 
		i = q ? q.length : 0, 
		x;
		while (i--){
			x = q[i];
			if (x.id==id){
				return {active:i, list:q.slice(Math.max(0, i-2), i+3)};
				}
			}
		return false;
		},
		
	findAndDisplay:function(type, fn, allowPartial){
	
		var obj = this.find(type),
		list = obj.list,
		l = list ? list.length : 0;

		if (								// don't update the cache when...
			l == 5 || 						// ...full set
			allowPartial || 				// ...cache already updated
			(blipData.streamAtStart && l-obj.active==3))		// ...at start of stream (page 1) and missing gaps are at the start
			{
		
			// originally we just displayed the entries even if there were less than 5. Instead, we now want to fill any gaps, always keeping the active image in the middle.
			if (l && l < 5){
				var i = obj.active, 
				img = list[i], 
				after = 2-(l-(i+1)),
				before = 5-(l+after);
				list = Array(before).concat(list, Array(after));
				}
		
			var ol = $('#stream ol').html($.makeArray($(list).map(function(i, e){
				return e ? '<li'+(e.id==blipData.id ? ' class="active"' : '')+'><a href="/entry/'+e.id+'?nav='+type+(blipData.streamGroup ? '&grp='+blipData.streamGroup.slug : '')+'"><img src="'+(e.id==blipData.id ? e.img.replace('gray','color') : e.img)+'" title="'+e.alt+'" /></a></li>' : '<li></li>';
				})).join(''));

			if (fn){
				
				var imgTimer, img=ol.find('img');
				
				// if the browser suppors the 'complete' property on images, we can detect when they're all loaded and fade them in nicely.
				if (img.length && ('complete' in img[0])){
				
					var start = +(new Date());			// because some thumbnails may not load (eg data stored in session, image gets deleted but session not updated) give 1 sec for the images to fully load, otherwise just show them anyway.
				
					(function(){
						if (start+1000 < +(new Date()) || !img.filter(function(k, i){
							return !i.complete;
							}).length){
							fn();
							clearTimeout(imgTimer);
							imgTimer = null;
							}
						else {
							imgTimer = setTimeout(arguments.callee, 50);
							}
						})();
						
					}
				else {
					// no easy .complete detection so just show them immediately, which may not look as nice.
					fn();
					}
				}
			}
			
		else {
		
			// if the number of items is less than 5, it may be there are no entries from the cache at all, we're reached the end of the cache, or we've reached the end of the stream.
			// Either way, we need to check the server now to update the cache.
		
			// couldn't find it in the queue, so fetch the data.
			$.post('/internal/ajax/entry/findinstream','id='+blipData.id+'&stream='+type+'&gid='+(blipData.streamGroup ? blipData.streamGroup.id : 0), function(r){
			
				blipData.stream[type]=$.parseJSON(r);
				
				// try finding within the new cache. Pass the 3rd argument to allow partials and prevent neverending loop.
				StreamQ.findAndDisplay(type, fn, true);
				});
			}
		},
		
	state:0,
	hover:function(){
		if (this.parentNode.parentNode.className!='active'){
			var s = this.src,
			a='gray', 
			b='color';
			if (s.match(a)){
				this.src = s.replace(a,b);
				}
			else {
				this.src = s.replace(b,a);
				}
			}
		},
		
	changeType:function(e, el){
	
		var t, 
		w=$('#stream ol');
	
		// get the new type;
		StreamQ.currentType = t = ((el || e.target).hash.replace('#',''));
		
		// highlight the correct link
		var links = $('#stream ul a').removeClass().filter('[href=#'+t+']').addClass('active');
		
		// fade out the image wrapper
		w.fadeOut(400, function(){
			
			// get rid of the images
			w.children().remove();
			
			StreamQ.findAndDisplay(t, function(){
				w.fadeIn();
				});
			
			});
		
		return false;
		},
		
	toggle:function(e){

		// if currently down, see if we need to initialise the gallery.
		if (blipData.stream && !StreamQ.state && StreamQ.currentType){
		
			var ol = $('#stream ol');
			if (!ol.children().length){			// if there's nothing there yet, we need to fill it before we show the element.
				ol.hide();
				StreamQ.findAndDisplay(StreamQ.currentType, function(){
					ol.fadeIn();
					});
				}
			
			}
		
		// if the event object is present, this call originated from a click so we should animate the transititon between states
		if (e){
			$('#stream div').animate({'height':(StreamQ.state ? 0 : 48)+'px'}, 300, 'swing');
			}

		StreamQ.state=!StreamQ.state;
	
		return false;
		}
	
	};
	
if (blipData.stream){
	StreamQ.init();

	// set hover action
	$('#stream ol img').live('mouseover', StreamQ.hover).live('mouseout', StreamQ.hover);
	
	// set type click action
	$('#stream ul a').live('click', StreamQ.changeType);

	}

// set up nav button click action
$('#streamnav').click(StreamQ.toggle);

if (blipData.streamVisible){
	StreamQ.toggle(null);
	}
