// Emacs settings: -*- mode: Fundamental; tab-width: 4; -*-

////////////////////////////////////////////////////////////////////////////
//                                                                        //
// Andrew's Album Applications: photos.js                                 //
//                                                                        //
// Uses functions from common.js and crypto.js                            //
//                                                                        //
// Copyright (c) 2004-2005, Andrew Birrell                                //
//                                                                        //
////////////////////////////////////////////////////////////////////////////


//
// Global variables
//

var wSize;					// window size
var mainSize = Object();	// size of main image display area
var iPhone = false;         // fine tuning of the UI

var dropZone;				// dropzone element
var reading;				// page element to show while reading a page
var scaling;				// page element to show current scale
var title;					// title element
var parents;				// element for parent links
var parentsTxt;				// HTML for top-level parent link
var writing;				// element for "writing" message
var editInner;				// inside of edit dialog
var editTitle;				// edit dialog title type-in element
var linkAnchor;				// <A> in "Link" button

var btns = new Object();	// button details
var dlogs = new Object();	// pop-up dialogs
var dlogPositioned = new Object();

var thisPage = null;		// attributes for current page

var user = "";				// default user for editing
var pwd = "";				// default password for editing
var jumpTarget = null;		// if set, switch to here instead
var timer = null;			// auto-play timer, for cancelling
var autoRoot = null;		// root directory for auto-play
var preloadServer = false;	// variant of auto-play to preload server cache
var interval = 6000;		// auto-play timer interval
var timerScaleOn = null;	// timer for showing "scaling" dlog
var timerScaleOff = null;	// timer for hiding "scaling" dlog

var cacheP = new Cache(50);	// cache of photo XML responses
var cacheF = new Cache(50);	// cache of folder XML responses
var cacheHits = 0;
var cacheMisses = 0;

var parentFrag =			// Template HTML fragment for parent link
	'<a href="#<#PATH>" onclick="' +
		"return doParent('<#PATH>')" +
		'" title="Go back to &quot;<#TITLE>&quot;"><#TITLE></a>';
var folderFrag =			// Template HTML fragment for folder listing
	'<td class=mini><a href="#<#PATH>" onclick="' +
		"return jumpTo('<#PATH>')" +
		'" title="Go to &quot;<#TITLE>&quot;">' +
		'<img class=mini width="<#WIDTH>" height="<#HEIGHT>" ' +
		'src="<#SRC>"></a></td>' +
	'<td class=miniTitle><a href="#<#PATH>" onclick="' +
		"return jumpTo('<#PATH>')" +
		'" title="Go to &quot;<#TITLE>&quot;"><#TITLE></a></td>';
var photoFrag =				// Template HTML fragment for photo listing
	'<a href="#<#PATH>" title="<#TITLE>" onclick="' +
		"return jumpTo('<#PATH>')" +
		'"><img class=thumb width="<#WIDTH>" height="<#HEIGHT>" ' +
		'src="<#SRC>" style="margin-left: <#MARGINL>px; ' +
		'margin-right: <#MARGINR>px; margin-top: <#MARGINT>px; ' +
		'margin-bottom: <#MARGINB>px"></a> ';
var mainFrag =				// Template HTML fragment for main screen
	'<a href="<#RAW>" target="_new"' +
//		' title="Download the original full-scale image<#SIZE>"' +
		'><img id=<#ID> ' +
		'style="width: 1px; height: 1px" src="<#SRC>"></a>';
var scalingFrag =
	'Image&nbsp;area is now<br><#XPX> x <#YPX> pixels';


//
// Dragging
//

var dragObject = null;		// victim of the current drag, if any
var dragHighlight;			// element whose class is changed during drag
var dragStartM = Object();	// initial mouse coordinates
var dragStartO;				// initial object coordinates, doc relative
var dragSaveClass;			// initial class of dragHighlight
var dragConstraint;

function dragStart(event, id, highlightId, highlightClass,
		constraint) {
	// Called on mousedown on the drag handle.
	// Sets things up to drag the element named by "id".
	// If "highlightId" is present (non-null), it's the name of an element
	// whose className should be changed to "highlightClass" during the
	// drag.
	dragObject = document.getElementById(id);
	if (dragObject) {
		dragStartO = getElementPos(dragObject);
		dragStartM.x = event.clientX;
		dragStartM.y = event.clientY;
		document.onmousemove = dragMove;
		document.onmouseup = dragStop;
		document.onmouseout = dragOut;
		if (highlightId) {
			dragHighlight = document.getElementById(highlightId);
		} else {
			dragHighlight = null;
		}
		if (dragHighlight) {
			dragSaveClass = dragHighlight.className;
			dragHighlight.className = highlightClass;
		}
	}
	dragConstraint = constraint;
	return false;
}

function dragMove(event) {
	// Called on mouse movement (in IE, only if it happens in our window)
	// Perform the drag
	if (dragObject) {
		if (!event) event = window.event; // IE versus the rest
		var lMin = (dragConstraint ? dragConstraint.lMin : 0);
		var lMax = (dragConstraint ? dragConstraint.lMax : 999999);
		var tMin = (dragConstraint ? dragConstraint.tMin : 0);
		var tMax = (dragConstraint ? dragConstraint.tMax : 999999);
		var newLeft = Math.max(lMin, Math.min(lMax,
			dragStartO.x + event.clientX - dragStartM.x));
		var newTop = Math.max(tMin, Math.min(tMax,
			dragStartO.y + event.clientY - dragStartM.y));
		dragObject.style.left = "" + newLeft + "px";
		dragObject.style.right = "auto";
		dragObject.style.top = "" + newTop + "px";
		dragObject.style.bottom = "auto";
		if (dragConstraint && dragConstraint.draggedTo) {
			dragConstraint.draggedTo(newLeft, newTop);
		}
	}
	return false;
}

function dragStop(event) {
	// Called on mouse up (in IE, only if it happens in our window)
	if (dragObject) {
		if (!event) event = window.event; // IE versus the rest
		if (dragHighlight) dragHighlight.className = dragSaveClass;
		dragObject = null;
		document.onmousemove = null;
		document.onmouseup = null;
		document.onmouseout = null;
		if (dragConstraint && dragConstraint.dragEnded) {
			dragConstraint.dragEnded();
		}
	}
	return false;
}

function dragOut(event) {
	// Called on mouse leaving its target element
	// We want to terminate the drag if the mouse leaves the window
	// (it can transiently leave dragObject during fast dragging)
	if (dragObject) {
		if (!event) event = window.event; // IE versus the rest
		if (!event.relatedTarget && !event.toElement) {
			// It didn't go anywhere, so it must have left completely
			dragStop(event);
		}
	}
	return false;
}


//
// Button management
//

function Button(id, action, photoTip, folderTip) {
	// Constructor for a Button object
	this.element = document.getElementById(id);
	if (this.element) {
		this.txt = this.element.innerHTML;
		this.photoHTML = '<a href="./" onclick="return ' +
			action + '" title="' + photoTip + '">' + this.txt + '</a>';
		this.folderHTML = '<a href="./" onclick="return ' +
			action + '" title="' + folderTip + '">' + this.txt + '</a>';
	}
	this.disable();
}

Button.prototype.enable = function() {
	// Enable a button
	if (this.element) {
		this.element.innerHTML = 
			(thisPage.isPhoto ? this.photoHTML : this.folderHTML);
	}
	this.enabled = true;
	this.visible = true;
}

Button.prototype.disable = function() {
	// Disable a button
	if (this.element) {
		this.element.innerHTML =
			'<span class=disabled>' + this.txt + '</span>';
	}
	this.enabled = false;
	this.visible = true;
}

Button.prototype.hide = function() {
	// Remove button from the screen
	if (this.element) this.element.innerHTML = "";
	this.enabled = false;
	this.visible = false;
}


//
// Progress bar animation and "reading" dialog
//
// This is purely for entertainment: it doesn't actually measure progress.
// It assumes that the bar widget should be sized within its parent.
//

var progressBarWidget = null;	// the widget being scaled
								// maximum width is the widget's parent
var progressBarTimer = null;	// timer for reading progress bar
var progressBarW = 0;			// reading progress bar state

function stepProgressBar() {
	// Animate the readProgress widget
	var maxWidth = progressBarWidget.parentNode.offsetWidth;
	progressBarW += (progressBarW <= 20 ? 4 :
		(progressBarW <= 40 ? 3 : 
			(progressBarW <= 60 ? 2 : 1)));
	if (progressBarW > maxWidth-4) progressBarW = 0;
	progressBarWidget.style.width = "" + progressBarW + "px";
}

function startProgressBar(widget) {
	// Start a progress bar using the element named "widget"
		progressBarWidget = document.getElementById(widget);
		progressBarWidget.style.width = "4px"; // amount initially visible
		progressBarW = 4;
		progressBarTimer = setInterval(stepProgressBar, 500);
}

function stopProgressBar() {
	// Stop animating the progress bar
	if (progressBarTimer) clearInterval(progressBarTimer);
	progressBarTimer = null;
}

function noteReading() {
	// Show the "reading" dialog and disable buttons as appropriate
	if (reading.style.display != 'block') {
		reading.style.display = "block";
		moveToCenter(reading, wSize);
		startProgressBar("readingWidget");
		btns.next.disable();
		btns.prev.disable();
		btns.skip.disable();
		if (btns.auto.visible) btns.auto.disable();
		btns.edit.disable();
		if (dlogs.editForm.style.display == 'block') {
			editTitle.blur();
			editInner.style.visibility = "hidden";
		}
	}
}

function endOfReading() {
	// enable/disable buttons as appropriate for current page,
	// and dispense with the "reading" dialog
	stopProgressBar();
	reading.style.display = "none";
	if (preloadServer) {
		btns.next.disable();
		btns.prev.disable();
		btns.skip.disable();
		btns.edit.disable();
	} else {
		if (thisPage.nextImage != "" || thisPage.isPhoto) {
			btns.next.enable();
		} else {
			btns.next.disable();
		}
		if (thisPage.prevImage != "" || thisPage.isPhoto) {
			btns.prev.enable();
		} else {
			btns.prev.disable();
		}
		if (thisPage.skipImage != "") {
			btns.skip.enable();
		} else {
			btns.skip.disable();
		}
		btns.edit.enable();
	}
	if (timer) {
		btns.auto.hide();
		btns.pause.enable();
		btns.resume.hide();
		btns.stop.enable();
	} else if (autoRoot) {
		btns.auto.hide();
		btns.pause.hide();
		btns.resume.enable();
		btns.stop.enable();
	} else {
		if (thisPage.autoImage != "") {
			btns.auto.enable();
		} else {
			btns.auto.disable();
		}
		btns.pause.hide();
		btns.resume.hide();
		btns.stop.disable();
	}
}


//
// Scaling the main image
//

function computeMainSize() {
	// Compute appropriate values for current size of "td.main" element
	// and adjust it in the DOM.
	// NOTE: in Safari 1.3.1, td.main is at the top of the image, which
	// can be below the top of the main table.  (In IE 6, both return the
	// same top value.)
	var mainTable = document.getElementById("mainTable");
	var main = document.getElementById("main");
	var bottom = document.getElementById("bottomWrapper");
	var mainPos = getElementPos(mainTable);
	wSize = windowSize();
	var h = wSize.y - mainPos.y - bottom.offsetHeight;
	main.style.height = "" + h + "px";
	mainSize.x = wSize.x;
	mainSize.y = h;
}

function scaleMainImage() {
	// Make the main image, if any, fit the "td.main" element
	// Used for onresize events, and when loading the page
	computeMainSize();
	var mainImage = document.getElementById(thisPage.path);
	if (mainImage) {
		if (thisPage.isPhoto) {
			var xScale = mainSize.x / thisPage.photoActualW;
			var yScale = mainSize.y / thisPage.photoActualH;
			// iPhone uses a viewscreen larger than the physicaly screen.
			// So images will be scaled down again, so we allow a scale-up.
			var scale = Math.min((iPhone ? 2 : 1),
							Math.min(xScale, yScale));
			mainImage.style.width = "" +
				Math.round(thisPage.photoActualW * scale) + "px";
			mainImage.style.height = "" +
				Math.round(thisPage.photoActualH * scale) + "px";
		} else {
			// iPhone's gesture for scrolling a fixed-size inner DIV is
			// awkward, and unknown by the vast majority of users.  So on an
			// iPhone we make the inner DIV height "auto".  However, td.main
			// has a fixed size too: if mainImage is less than that, the
			// size from td.main applies, placing the bottom menu bar at
			// the bottom of the screen; if mainImage is more than that,
			// "auto" makes it expand td.main.
			if (iPhone) {
				mainImage.style.height = "auto";
			} else {
				mainImage.style.height = "" + mainSize.y + "px";
			}
		}
	}
}

function scaleOn() {
	timerScaleOn = null;
	if (!iPhone) {
		// Don't show scale on iPhone ... it happens too often, and isn't
		// readable anyway.
		scaling.style.display = 'block';
		var temp = scalingFrag.replace(/<#XPX>/, mainSize.x);
		temp = temp.replace(/<#YPX>/, mainSize.y);
		scaling.innerHTML = temp;
		moveToCenter(scaling, wSize);
		if (timerScaleOff) clearTimeout(timerScaleOff);
		timerScaleOff = window.setTimeout(scaleOff, 1000);
	}
}

function scaleOff() {
	if (scaling.style.display == 'block') scaling.style.display = 'none';
}

function doResize() {
	// Respond to window resizing
	scaleOff();
	if (thisPage) {
		scaleMainImage();
		// Showing the "scaling" window during resize causes significant
		// flicker in IE, so we defer it until dragging stops
		if (timerScaleOn) clearTimeout(timerScaleOn);
		timerScaleOn = window.setTimeout(scaleOn, 100);
	}
}


//
// Prefetching Images
//

// We could get images pre-loaded by placing them all into some HTML
// and handing that to the browser.  But then there'd be no way
// to cancel the load requests if we want to go somewhere else.
// By doing them sequentially ourselves, we get that control.
//
// This doesn't work on iPhone (v.2).

var imgQueue = null;				// queue of images to preload
var imgQueueTail = null;

function initiateImageLoad() {
	if (imgQueue) {
		var prefetch = document.getElementById("prefetch");
		prefetch.onload = stepImagePreload;
		prefetch.src = imgQueue.src;
	}
}

function  stepImagePreload() {
	// Current preload has completed; initiate next
	imgQueue = imgQueue.next;
	if (imgQueue) {
		// execute next asynchronously, to avoid recursive overflow
		setTimeout(initiateImageLoad, 10);
	}
}

function enqueueImage(src) {
	// enqueue an image for preloading
	if (!iPhone) {
		var queue = new Object();
		queue.src = src;
		queue.next = null;
		if (imgQueue) {
			imgQueueTail.next = queue;
		} else {
			imgQueue = queue;
			initiateImageLoad();
		}
		imgQueueTail = queue;
	}
}

function cancelImagePreload() {
	// Dispense with the image preload queue
	if (imgQueue) imgQueue.next = null;
}


//
// Page interpretation
//

function getTitle(node) {
	// Return the value of the "title" child of given XML node
	var titles = node.getElementsByTagName("title");
	var child = titles[0].firstChild;
	// The title is the child, but there's no child if the title was empty
	return (child ? child.nodeValue : "");
}

function buildPage(responseXML) {
	// Construct and return a page object based on the given XML
	var doc = responseXML.documentElement;
	newPage = new Object();
	newPage.responseXML = responseXML;
	newPage.path = doc.getAttribute("path");
	newPage.title = getTitle(doc);
	newPage.nextImage = doc.getAttribute("next");
	newPage.prevImage = doc.getAttribute("prev");
	newPage.skipImage = doc.getAttribute("skip");
	var starLevel = doc.getAttribute("starLevel");
	newPage.starLevel = (starLevel ? parseInt(starLevel) : null);
	newPage.parents = ''; // parent links HTML
	var i;
	var parents = doc.getElementsByTagName("parent");
	newPage.lastParent = "";
	for (i = 0; i < parents.length; i++) {
		var child = parents[i];
		var chtitle = getTitle(child);
		var chpath = child.getAttribute("path");
		if (i == 0) {
			chtitle = parentsTxt;
		} else {
			newPage.parents += '&nbsp; &gt;&nbsp; ';
		}
		var temp = parentFrag.replace(/<#PATH>/g, chpath);
		temp = temp.replace(/<#TITLE>/g, htmlspecials(chtitle));
		newPage.parents += temp;
		newPage.lastParent = chpath;
		newPage.autoRoot = chpath; // use last parent for photos
	}
	if (doc.nodeName == "photo") {
		newPage.isPhoto = true;
		newPage.autoImage = // enable auto iff next is in this folder
			(newPage.autoRoot == "." ||
				newPage.nextImage.indexOf(newPage.autoRoot) == 0 ?
					newPage.nextImage : "");
		var width = parseInt(doc.getAttribute("width"));
		var height = parseInt(doc.getAttribute("height"));
		var src = doc.getAttribute("src");
		var raw = doc.getAttribute("raw");
		var size = doc.getAttribute("size");
		var date = doc.getAttribute("date");
		var exposure = doc.getAttribute("exposure");
		newPage.photoActualW = width;
		newPage.photoActualH = height;
		if (preloadServer) {
			newPage.html = "<br><br><br><br><br><br><br><br>" +
				"Preloading the server-side cache," +
				" with no image display and no delays.<p>" +
				"Click \"Stop\" to exit from this mode.";
		} else {
			var temp = mainFrag.replace(/<#SRC>/, src);
			temp = temp.replace(/<#RAW>/, raw);
			temp = temp.replace(/<#ID>/, newPage.path);
			temp = temp.replace(/<#SIZE>/, "\n" + size);
			newPage.html = temp;
		}
		newPage.commentary = (date ? htmlspecials(date) : "&nbsp;") +
			"<br>" +
			(exposure ? htmlspecials(exposure) : "&nbsp;")
	} else {
		// folder
		newPage.isPhoto = false;
		if (parents.length > 0) {
			newPage.parents += '&nbsp; &gt;&nbsp; ';
		}
		newPage.parents += '<span class=disabled>' +
			(parents.length == 0 ? parentsTxt :
						htmlspecials(newPage.title)) +
			'</span>';
		newPage.autoImage = doc.getAttribute("first");
		newPage.autoRoot = newPage.path; // use self for folders
		content = '<div class=catalog id="' + newPage.path + '">';
		var children = doc.getElementsByTagName("folder");
		var folderCount = children.length;
		var lCol = 0; // child index for left column
		var rCol = Math.round((folderCount+0.5)/2);
		var i;
		for (i = 0; i < folderCount; i++) {
			var sub;
			if (i%2 == 0) {
				content += (i == 0 ?
					"<table class=catalog cellspacing=0>" :
					"</tr>");
				content += "<tr>";
				sub = children[lCol]; lCol++;
			} else {
				sub = children[rCol]; rCol++;
			}
			var chpath = sub.getAttribute("path");
			var chtitle = getTitle(sub);
			var width = sub.getAttribute("width");
			var height = sub.getAttribute("height");
			var src = sub.getAttribute("src");
			var temp = folderFrag.replace(/<#PATH>/g, chpath);
			temp = temp.replace(/<#WIDTH>/, width);
			temp = temp.replace(/<#HEIGHT>/, height);
			temp = temp.replace(/<#SRC>/, src);
			temp = temp.replace(/<#TITLE>/g, htmlspecials(chtitle));
			content += temp;
		}
		if (i != 0) content += "</tr></table>";
		var photoMaxW = parseInt(doc.getAttribute("photoMaxW"));
		children = doc.getElementsByTagName("photo");
		var photoCount = children.length;
		for (i = 0; i < photoCount; i++) {
			var sub = children[i];
			var chpath = sub.getAttribute("path");
			var chtitle = getTitle(sub);
			var width = parseInt(sub.getAttribute("width"));
			var height = parseInt(sub.getAttribute("height"));
			var src = sub.getAttribute("src");
			var temp = photoFrag.replace(/<#PATH>/g, chpath);
			temp = temp.replace(/<#TITLE>/g, htmlspecials(chtitle));
			temp = temp.replace(/<#WIDTH>/, width);
			temp = temp.replace(/<#HEIGHT>/, height);
			temp = temp.replace(/<#SRC>/, src);
			var lMargin = 6 + Math.floor((photoMaxW-width)/2);
			// Vertically, we can just use a fixed margin, because our style
			// lays the images out with "vertical-align: center".  This
			// way rows are closer together when there are no portrait-mode
			// photos.
			temp = temp.replace(/<#MARGINL>/, lMargin);
			temp = temp.replace(/<#MARGINR>/, 6+photoMaxW-(width+lMargin));
			temp = temp.replace(/<#MARGINT>/, 4);
			temp = temp.replace(/<#MARGINB>/, 0);
			content += temp;
		}
		newPage.commentary =
			(folderCount > 0 ? "" + folderCount + " folders" : "&nbsp;") +
			(folderCount > 0 && photoCount > 0 ? " and" : "") +
			"<br>" +
			(photoCount > 0 ? "" + photoCount + " photographs" : "&nbsp;")
		newPage.html = content + '<br>&nbsp;</div>';
	}
	return newPage;
}

function displayPage() {
	// Update the display based on the current page attributes
	setCookie("AndrewAlbumApp", thisPage.path);
	var commentary = document.getElementById("commentary");
	if (thisPage.commentary) {
		commentary.innerHTML = thisPage.commentary;
	} else {
		commentary.innerHTML = "&nbsp;<br>&nbsp;";
	}
	dropZone.innerHTML = thisPage.html;
	window.scrollTo(0,0);
	title.innerHTML = htmlspecials(thisPage.title);
	parents.innerHTML = thisPage.parents;
	showStarLevel();
	editTitle.value = thisPage.title;
	if (dlogs.editForm.style.display == 'block') {
		editInner.style.visibility = "visible";
		editTitle.focus();
		editTitle.select();
	}
	scaleMainImage();
	linkAnchor.href = '#' + thisPage.path;
	endOfReading();
}


//
// Page access
//

var fetching = new Object(); // paths currently being fetched from server

function getXML(path, jumpTo) {
	// Fetch XML for given path.  If it's already in cache, return the XML;
	// if it's already being fetched, do nothihng, otherwise initiate fetch
	// "path" was urlencoded by the server-side script.
	// Iff "jumpTo", arrange to display the page when it arrives.
	var cached = cacheP.read(path);
	if (!cached) cached = cacheF.read(path);
	if (cached) {
		if (jumpTo) jumpTarget = null; // cancel any in-progress jumpTarget
		return cached;
	}
	if (jumpTo) {
		jumpTarget = path;
		noteReading();
	}
	if (!fetching[path]) {
		var thisReq = new Object();
		thisReq.requestedPath = path;
		fetching[path] = thisReq;
		get(thisReq, "photos.php?op=xml&path=" + path +
			(iPhone ? "&pda=" : ""));
	}
}

function loadPage(responseXML) {
	// Load the given page's responseXML into the window.
	// Also initiate page prefetching, and refresh auto-display timer
	thisPage = buildPage(responseXML);
	cancelImagePreload();
	fetching = new Object(); // abandon previous page prefetching
	displayPage();
	if (thisPage.path == autoRoot) manual();
	if (!preloadServer) {
		// pre-fetch nearby pages
		if (thisPage.autoImage != "") getXML(thisPage.autoImage, false);
		if (thisPage.nextImage != "") getXML(thisPage.nextImage, false);
		if (thisPage.lastParent != "") getXML(thisPage.lastParent, false);
	}
	if (timer) timer =
		window.setTimeout(doNext, (preloadServer ? 10 : interval));
}

function handleFailure(thisReq) {
	// Completion procedure called on failures
	// Redisplay the current page (undo "showReading")
	cancelImagePreload();
	fetching = new Object();
	if (thisPage) displayPage();
}

function handleResult(thisReq) {
	// Completion procedure: interpret the result, assumed to be XML
	var responseXML = thisReq.xmlhttp.responseXML;
	if (!responseXML) alert(thisReq.xmlhttp.responseText);
	var doc = responseXML.documentElement;
	if (doc.nodeName == "save") {
		// Result of editSave
		var status = doc.getAttribute("status");
		var path = doc.getAttribute("path");
		if (thisReq.isPhoto) {
			cacheP.flush(path);
		} else {
			cacheP = new Cache(50);  // parent names might have changed
			cacheF = new Cache(50);
		}
		editInner.style.visibility = "visible";
		writing.style.display = 'none';
		if (status == "ok") {
			if (editStayOpen) {
				doNext();
			} else {
				closeDlogs();
				jumpTo(thisPage.path);
			}
		} else {
			alert("Failed: " + status);
		}
	} else if (doc.nodeName == "photo" || doc.nodeName == "folder") {
		// Interpret the XML response for a folder or image page
		var path = doc.getAttribute("path");
		var thisCache = (doc.nodeName == "photo" ? cacheP : cacheF);
		thisCache.write(path, responseXML);
		if (jumpTarget == thisReq.requestedPath) {
			// Note that the server can return a page with a different
			// path, for example if the one we requested doesn't exist
			jumpTarget = null;
			loadPage(responseXML);
		} else if (fetching[path]) {
			fetching[path] = false;
			// enqueue preload requests for our images, if we're still OK
			if (preloadServer) {
				// don't preload any images
			} else if (doc.nodeName == "photo") {
				var src = doc.getAttribute("src");
				enqueueImage(src);
			} else { // folder
				var children = doc.getElementsByTagName("folder");
				for (var i = 0; i < children.length; i++) {
					var sub = children[i];
					var src = sub.getAttribute("src");
					enqueueImage(src);
				}
				children = doc.getElementsByTagName("photo");
				for (var i = 0; i < children.length; i++) {
					var sub = children[i];
					var src = sub.getAttribute("src");
					enqueueImage(src);
				}
			}
		}
	} else {
		alert('Unknown result, nodeName="' + doc.nodeName +'"');
	alert(thisReq.xmlhttp.responseText);
		handleFailure(thisReq);
	}
}

function jumpTo(path) {
	// Jump to given path
	if (timer) clearTimeout(timer);
	var cached = getXML(path, true);
	if (cached) {
		cacheHits++;
		loadPage(cached);
	} else {
		cacheMisses++;
	}
	return false;
}


//
// The top-level subroutines, mostly invoked from the HTML page
//

function containedJump(candidate) {
	// If candidate would move us outside of our folder (or autoRoot),
	// jump to the bounding folder; otherwise jump to the candidate
	var target = candidate;
	var container = (autoRoot ? autoRoot :
						(thisPage.isPhoto ? thisPage.lastParent : null));
	if (container) {
		if (container == ".") {
			if (candidate == "") target = container;
		} else {
			if (candidate.indexOf(container) != 0) target = container;
		}
	}
	if (target != "") jumpTo(target);
}

function doNext() {
	// Perform the "next" operation.
	containedJump(thisPage.nextImage);
	return false;
}

function doPrev() {
	containedJump(thisPage.prevImage);
	return false;
}

function doSkip() {
	containedJump(thisPage.skipImage);
	return false;
}

function auto(event) {
	// start automatic mode
	autoRoot = thisPage.autoRoot;
	preloadServer = (event.shiftKey ? true : false);
	if (preloadServer) {
		// flush cache so that we actually use the server-side
		cacheP = new Cache(50);
		cacheF = new Cache(50);
	}
	if (!timer) timer = window.setTimeout("jumpTo(thisPage.autoImage)", 50);
	btns.auto.hide();
	btns.pause.enable();
	btns.resume.hide();
	btns.stop.enable();
	return false;
}

function pause() {
	// suspend automatic mode without losing track of root
	if (timer) clearTimeout(timer);
	timer = null;
	btns.auto.hide();
	btns.pause.hide();
	btns.resume.enable();
	btns.stop.enable();
	return false;
}

function resume() {
	// continue after pause
	if (!timer) timer = window.setTimeout(doNext, 50);
	btns.auto.hide();
	btns.pause.enable();
	btns.resume.hide();
	btns.stop.enable();
	return false;
}

function manual() {
	// stop automatic mode
	if (timer) clearTimeout(timer);
	timer = null;
	var wasPreloading = preloadServer;
	if (preloadServer) {
		// flush cache so that future pre-fetches will pre-fetch images
		cacheP = new Cache(50);
		cacheF = new Cache(50);
	}
	autoRoot = null;
	preloadServer = false;
	btns.auto.enable();
	btns.pause.hide();
	btns.resume.hide();
	btns.stop.disable();
	if (wasPreloading && thisPage) jumpTo(thisPage.path);
	return false;
}

function doParent(path) {
	// Jump to given path, cancelling auto-play
	if (autoRoot) manual();
	jumpTo(path);
	return false;
}

function up() {
	// Go up one folder level
	if (thisPage.lastParent != "") doParent(thisPage.lastParent);
	return false;
}

function hideShowActions() {
	// Hide or show the "actions" menu body
	var body = document.getElementById("actionsBody");
	var openClose = document.getElementById("openClose");
	if (body.style.display == 'none') {
		body.style.display = 'block';
		openClose.src = 'open.gif';
	} else {
		body.style.display = 'none';
		openClose.src = 'closed.gif';
	}
	return false;
}

function closeDlogs() {
	// Close all dialog elements
	for (var dlog in dlogs) {
		var style = dlogs[dlog].style;
		if (style.display != 'none') style.display = 'none';
	}
	return false;
}

function openDlog(dlogName) {
	closeDlogs();
	dlog = dlogs[dlogName];
	dlog.style.display = 'block';
	if (!dlogPositioned[dlogName]) {
		moveToCenter(dlog, wSize);
		dlogPositioned[dlogName] = true;
	}
	if (dlog == dlogs.editForm) {
		if (timer) pause();
		var editUser = document.getElementById("editUser");
		var editPwd = document.getElementById("editPwd");
		editUser.value = user;
		editPwd.value = pwd;
		if (user == "") {
			editUser.focus();
		} else if (pwd == "") {
			editPwd.focus();
		} else {
			editTitle.focus();
		}
	}
	return false;
}

var editStayOpen; // leave the dialog up after save completion

function editSave(stayOpen) {
	editStayOpen = stayOpen;
	user = document.getElementById("editUser").value;
	pwd = document.getElementById("editPwd").value;
	setCookie("pachyuser", user, null, "/");
	setCookie("photopwd", pwd, null, "/", null, true);
	var title = document.getElementById("editTitle").value;
	editInner.style.visibility = "hidden";
	writing.style.display = 'block';
	if (thisPage.isPhoto) {
		cacheP.flush(thisPage.path);
		cacheF.flush(thisPage.lastParent);
	} else {
		cacheP = new Cache(50);  // parent names might have changed
		cacheF = new Cache(50);
	}
	var now = new Date();
	var nowSec = Math.round(now.getTime() / 1000);
	var hmac = md5HmacHex(utf8("AndrewAlbumApp\x00xmlSave\x00" +
			user + "\x00" + nowSec + "\x00" + thisPage.path + "\x00" +
			title),
			utf8(pwd));
	var saveRequest = new Object();
	saveRequest.isPhoto = thisPage.isPhoto;
	get(saveRequest,
		"photos.php", "path=" + thisPage.path + "&op=xmlSave&title=" +
			encodeURIComponent(title) +
			"&user=" + encodeURIComponent(user) +
			"&time=" + nowSec +
			"&hmac=" + hmac);
	return false;
}

function starNumber(star) {
	var id = star.id;
	return parseInt(id.substring(4));
}

function setStarDisplay(n, src) {
	for (var i = 1; i <= n; i++) {
		var x = document.getElementById("star" + i);
		x.src = src;
	}
	for (var i = n+1; i <= 5; i++) {
		var x = document.getElementById("star" + i);
		x.src = "whiteStar.gif";
	}
}

function showStarLevel() {
	// display current star level, or hide the stars
	var stars = document.getElementById("stars");
	if (!thisPage || thisPage.starLevel == null) {
		stars.style.display = "none";
	} else {
		setStarDisplay(thisPage.starLevel, "redStar.gif");
		stars.style.display = "block";
	}
}

function starOver(star) {
	// mouse over a star
	setStarDisplay(starNumber(star), "blueStar.gif");
}

function starClick(star) {
	// mouse click on a star
	var n = starNumber(star);
	thisPage.starLevel = n;
	thisPage.responseXML.documentElement.setAttribute("starLevel", n);
	showStarLevel();
}

function init() {
	if (!document.getElementById) {
		alert('This browser has no "getElementById" support.' +
			' Redirecting to the non-scripted page');
		location.replace("noscript.php");
		return;
	}
	dropZone = document.getElementById("dropZone");
	reading = document.getElementById("reading");
	scaling = document.getElementById("scaling");

	title = document.getElementById("titleText");
	title.innerHTML = "";
	parents = document.getElementById("parents");
	parentsTxt = parents.innerHTML;
	parents.innerHTML = "";
	showStarLevel();

	dlogs.about = document.getElementById("about");
	dlogs.details = document.getElementById("details");
	dlogs.editForm = document.getElementById("editForm");
	dlogs.link = document.getElementById("link");

	writing = document.getElementById("writing");
	editInner = document.getElementById("editInner");
	editTitle = document.getElementById("editTitle");
	linkAnchor = document.getElementById("linkAnchor");

	btns.auto = new Button("auto", "auto(event)",
		"Play all the photos in this folder, from here onward",
		"Play all the photos in this folder, including " +
		"those in its sub-folders");
	btns.pause = new Button("pause", "pause()",
		"Suspend the slideshow, remembering the starting position",
		"I'm not here");
	btns.pause.hide();
	btns.resume = new Button("resume", "resume()",
		"Resume the slideshow",
		"I'm not here");
	btns.resume.hide();
	btns.stop = new Button("stop", "manual()",
		"Stop the slideshow",
		"I'm not here");
	btns.prev = new Button("prev", "doPrev()",
		"Move to the previous photo",
		"Move to the previous folder");
	btns.next = new Button("next", "doNext()",
		"Move to the next photo",
		"Move to the next folder, including sub-folders");
	btns.skip = new Button("skip", "doSkip()",
		"Skip the remainder of this folder",
		"Move to the next folder, skipping this folder's sub-folders");
	btns.edit = new Button("edit", "openDlog('editForm')",
		"Edit this photo's title",
		"Edit this folder's title");

	iPhone = isIphone();
	if (iPhone) interval += 1000; // compensate for no image pre-fetch
	window.onresize = doResize;

	var dest = "";
	if (location.hash && location.hash != "") {
	  	dest = location.hash.substring(1);
	} else {
		var prevPath = getCookie("AndrewAlbumApp");
		if (prevPath) dest = prevPath;
	}
	user = getCookie("pachyuser");
	if (!user) user = "";
	dropZone.innerHTML = "";
	jumpTo(dest);

	// Enable the UI
	document.getElementById("parentBtns").style.visibility = "visible";
	document.getElementById("prevnextBtns").style.visibility = "visible";
	document.getElementById("privBtns").style.visibility = "visible";
}
