// 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 autoInterval = 4000;	// auto-play timer interval
var menubarDelay = 4000;	// menubar removal delay

// Various DOM elements, cached for code simplicity
//
var reading;				// page element to show while reading a page
var scaling;				// page element to show current scale
var title;					// title element
var parentBtns;				// 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

// Data structures
//
var btns = new Object();	// button details
var dlogs = new Object();	// pop-up dialogs
var dlogPositioned = new Object();
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 swiper = new Object();	// machinery for swiping on touch screens

// UI tweaks for small screens (set in "init")
//
var menubarsAuto = false;   // whether to auto-hide the menu bars
var iconDownscale = 1;      // ability to use smaller photo icons
var pdaImageSize = "";      // option to download a smaller main image

// Mutable UI state
//
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 timerScaleOn = null;	// timer for showing "scaling" dlog
var timerScaleOff = null;	// timer for hiding "scaling" dlog
var menubarsVisible = true;	// visibility of the menu bars
var menubarTimer = null;	// timer for hiding the menu bars

// Templates for dynamically generated HTML
//
var parentFrag =			// Template HTML fragment for parent link
	'<a href="#<#PATH>" onclick="' +
		"return doParent('<#PATH>')" +
		'" title="Go back to &quot;<#TITLE>&quot;">Up: ' +
		'<span id=uptitle><#TITLE></span></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 photo itself
//	'<a href="<#RAW>" target="_new"' +
//		' title="Download the original full-scale image<#SIZE>">' +
//		'<img id=<#ID> style="width: 1px; height: 1px" src="<#SRC>">' +
//	'</a>';
// or ...
//	'<img id=<#ID> style="width: 1px; height: 1px" src="<#SRC>">';
// but ...
	'<div  class=mainphoto id=mainWrapper onclick="return photoClick()">' +
		'<img id=<#ID> style="width: 1px; height: 1px" src="<#SRC>">' +
	'</div>';
var scalingFrag =			// Template for size reporting pop-up
	'Image&nbsp;area is now<br><#XPX> x <#YPX> pixels';


//
// Dragging dialog boxes, just for fun
//

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;
}


//
// showing and hiding the menu bars
//

function startMenubarTimer() {
	// Arrange to hide the menu bars in a while
	//
	if (menubarsAuto && !menubarTimer && menubarsVisible) {
		menubarTimer = window.setTimeout(
			function() { showHideMenubars(false); },
			menubarDelay);
	}
}

function cancelMenubarTimer() {
	// Cancel timeout on menubar
	//
	if (menubarTimer) {
		clearTimeout(menubarTimer);
		menubarTimer = null;
	}
}

function refreshMenubar() {
	// Refresh timeout on menu bar. if there is one
	//
	if (menubarTimer) {
		cancelMenubarTimer();
		startMenubarTimer();
	}
}

function showHideOneMenubar(elt, opacity) {
	// Make one menu bar visible or not
	//
	elt.style.webkitTransitionDuration = (opacity > 0 ? "0ms" : "750ms");
	elt.style.opacity = "" + opacity;
	elt.style.filter = "alpha(opacity=" +
		Math.round(opacity * 100) +
		")"; // For IE
}

function showHideMenubars(visible) {
	// Make the menu bars visible or not; cancels any menubarTimer
	//
	cancelMenubarTimer();
	var topStuff = document.getElementById("topStuff");
	var bottomStuff = document.getElementById("bottomStuff");
	if (visible != menubarsVisible) {
		if (menubarsVisible) {
			showHideOneMenubar(topStuff, 0);
			showHideOneMenubar(bottomStuff, 0);
		} else {
			showHideOneMenubar(topStuff, 0.80);
			showHideOneMenubar(bottomStuff, 0.80);
		}
		menubarsVisible = visible;
		if (!menubarsAuto && thisPage) scaleMainImage();
	}
	return true;
}


//
// Swipe dragging of photos, for devices with touch screens
//

function swipeOK(which) {
	// Return "which" iff a swipe in the given direction is OK right now,
	// else return 0.  Provided by the client of the swiper.
	//
	// (which=-1 when swiping to the left, +1 when swiping to the right.)
	//
	refreshMenubar();
	return ((which < 0 && btns.next.enabled) ||
			(which > 0 && btns.prev.enabled) ? which : 0);
}

function swipeAction(which) {
	// Implement the action of a swipe in the given direction.  Provided by
	// the client of the swiper.
	//
	// (which=-1 when swiping to the left, +1 when swiping to the right.)
	//
	if (which < 0) {
		doNext();
	} else {
		doPrev();
	}
}

// The remainder of the swiper code is re-usable.

function swipeInitOne(id) {
	var elt = document.getElementById(id);
	if (elt.addEventListener) {
		elt.addEventListener('touchstart', swipeTouchStart, false);
	}
	return elt;
}

function swipeMoveOne(elt, pos, msecTxt) {
	// Transform one photo to given position
	elt.style.webkitTransitionDuration = msecTxt;
	elt.style.webkitTransform =
		"translate(" + pos.x + "px, " + pos.y + "px)";
}

function swipeInit(cur, prev, next) {
	swiper.cur = swipeInitOne(cur);
	swiper.prev = swipeInitOne(prev);
	swiper.next = swipeInitOne(next);
	swiper.vertical = false; // direction in which we're swiping
}

function swipeConstrain(pos) {
	// constrain "pos" to the dimension in which we're swiping
	if (swiper.vertical) pos.x = 0; else pos.y = 0;
}

function swipeEdge(which, delta) {
	// Return canonical top-left position for (prev,cur,next) as
	// which = (-1,0,1), adjusted by delta.
	var pos = windowSize();
	pos.x *= which;
	pos.y *= which;
	if (delta) {
		pos.x += delta.x;
		pos.y += delta.y;
	}
	swipeConstrain(pos);
	return pos;
}

function swipeMoveTo(pos, msec) {
	// translate to given position, in given milliseconds
	var msecText = (msec && msec > 0 ? msec : 0) + "ms";
	swipeMoveOne(swiper.cur, pos, msecText);
	swipeMoveOne(swiper.prev, swipeEdge(-1, pos), msecText);
	swipeMoveOne(swiper.next, swipeEdge(1, pos), msecText);
}

function swipeGetPos() {
	// Return current position of cur
	var theTransform =
			window.getComputedStyle(swiper.cur).webkitTransform;
	var theMatrix = new WebKitCSSMatrix(theTransform);
	return {
		x: theMatrix.m41,
		y: theMatrix.m42
	};
}

function swipeTouchStart(event) {
	if (event.targetTouches.length != 1) return false;
	var theTouch = event.targetTouches[0];
	swiper.startClientX = theTouch.clientX;
	swiper.startClientY = theTouch.clientY;
	swiper.startPos = swipeGetPos();
	swiper.moved = false;
	swiper.cur.addEventListener('touchmove', swipeTouchMove, false);
	swiper.cur.addEventListener('touchend', swipeTouchEnd, false);
	return false;
}

function swipeTouchMove(event) {
	if (event.targetTouches.length != 1) return false;
	var theTouch = event.targetTouches[0];
	var pos = {
		x: swiper.startPos.x + theTouch.clientX - swiper.startClientX,
		y: swiper.startPos.y + theTouch.clientY - swiper.startClientY
	};
	if (!swiper.moved && (swiper.vertical ?
			Math.abs(pos.x) > Math.abs(pos.y) :
			Math.abs(pos.y) > Math.abs(pos.x))) {
		// Abandon the touch and leave it to the default.  This enables
		// native scrolling in the non-swiper axis.
		swiper.cur.removeEventListener('touchmove', swipeTouchMove, false);
		swiper.cur.removeEventListener('touchend', swipeTouchEnd, false);
		return true;
	}	
	event.preventDefault();
	swipeConstrain(pos);
	var which = (pos.x + pos.y < 0 ? -1 : (pos.x + pos.y > 0 ? 1 : 0));
	which = swipeOK(which);
	if (which == 0) { // swipe not applicable, so slip a bit
		pos.x = Math.round(pos.x / 2);
		pos.y = Math.round(pos.y / 2);
	}
	swiper.prev.style.display = "block";
	swiper.next.style.display = "block";
	swipeMoveTo(pos, 0);
	swiper.moved = true;
	return false;
}

function swipeTouchEnd(event) {
	swiper.cur.removeEventListener('touchmove', swipeTouchMove, false);
	swiper.cur.removeEventListener('touchend', swipeTouchEnd, false);
	if (swiper.moved) event.preventDefault();
		// The native code handles conversion of non-moves into clicks
	var pos = swipeGetPos();
	// Note that pos and endPos have x=0 or y=0, depending on swipe axis
	var which = (pos.x + pos.y < -20 ? -1 : (pos.x + pos.y > 20 ? 1 : 0));
	which = swipeOK(which);
	var endPos = swipeEdge(which);
	var nextPos = swipeEdge(1);
	var width = nextPos.x + nextPos.y;
	var distance = Math.abs(endPos.x - pos.x + endPos.y - endPos.y);
	var duration = Math.round(300 * distance / width);
	swiper.cur.addEventListener('webkitTransitionEnd', swipeTranEnd, false);
	swipeMoveTo(endPos, (duration > 0 ? duration : 1));
		// "1" forces a callback to "onTransitionEnd"

	swiper.oldCur = swiper.cur;
	if (which < 0) {
		swiper.cur = swiper.next;
		swiper.next = swiper.prev;
		swiper.prev = swiper.oldCur;
	} else if (which > 0) {
		swiper.cur = swiper.prev;
		swiper.prev = swiper.next;
		swiper.next = swiper.oldCur;
	}
	if (which != 0) swipeAction(which);

	return false;
}

function swipeTranEnd(event) {
	swiper.oldCur.removeEventListener('webkitTransitionEnd', swipeTranEnd,
										false);
	swiper.prev.style.display = "none";
	swiper.next.style.display = "none";
	swiper.prev.style.backgroundColor = swiper.cur.style.backgroundColor;
	swiper.next.style.backgroundColor = swiper.cur.style.backgroundColor;
	swiper.prev.innerHTML = "";
	swiper.next.innerHTML = "";
}


//
// 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 ? folderTip : photoTip) + '">' + this.txt + '</a>';
	}
	this.disable();
	btns[id] = this;
}

Button.prototype.enable = function() {
	// Enable a button
	if (this.element) {
		this.element.innerHTML =  (thisPage && 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, windowSize());
		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 != "" &&
				((autoRoot && autoRoot != thisPage.lastParent) ||
				(thisPage.subFolders && thisPage.subFolders.length > 0))) {
			btns.skip.enable();
		} else {
			btns.skip.disable();
		}
		btns.edit.enable();
	}
	if (timer) {
		btns.auto.hide();
		if (autoRoot != thisPage.lastParent && !preloadServer) {
			btns.pause.enable();
			btns.stop.hide();
		} else {
			btns.pause.hide();
			btns.stop.enable();
		}
		btns.resume.hide();
	} else if (autoRoot) {
		btns.auto.hide();
		btns.pause.hide();
		btns.resume.enable();
		btns.stop.hide();
	} else {
		if (thisPage.autoImage != "") {
			btns.auto.enable();
		} else {
			btns.auto.disable();
		}
		btns.pause.hide();
		btns.resume.hide();
		btns.stop.hide();
	}
}


//
// Scaling the main image
//

function scaleMainImage() {
	// Make the main image, if any, fit the "td.main" element
	// Used for onresize events, and when loading the page
	//
	var wSize = windowSize();
	scaleOn(wSize);
	var contentY = wSize.y;
	swiper.cur.style.height = "" + contentY + "px";
	swiper.prev.style.height = "" + contentY + "px";
	swiper.next.style.height = "" + contentY + "px";
	var topBar = document.getElementById("topStuff");
	var bottomBar = document.getElementById("bottomStuff");
	if (thisPage.isPhoto) {
		var topH = 0;
		var bottomH = 0;
		if (!menubarsAuto && menubarsVisible) { // pad under the menu bars
			topH = topBar.offsetHeight;
			bottomH = bottomBar.offsetHeight;
		}
		var photoSize = {x: wSize.x, y: wSize.y};
		photoSize.y -= topH + bottomH;
		var mainImage = document.getElementById(thisPage.path);
		var xScale = photoSize.x / thisPage.photoActualW;
		var yScale = photoSize.y / thisPage.photoActualH;
		var scale = Math.min(1, Math.min(xScale, yScale));
		var h = Math.round(thisPage.photoActualH * scale);
		var w = Math.round(thisPage.photoActualW * scale);
		var padY = photoSize.y - h;
		var padTop = topH + Math.round(padY/2);
		var padX = wSize.x - w;
		var padLeft = Math.round(padX/2);
		mainImage.style.width = "" + w + "px";
		mainImage.style.height = "" + h + "px";
		mainImage.style.paddingTop = "" + padTop + "px";
		mainImage.style.paddingBottom = "" + (bottomH+padY-padTop) + "px";
		mainImage.style.paddingLeft = "" + padLeft + "px";
//		mainImage.style.paddingRight = "" + (padX-padLeft) + "px";
// Not needed: the containing DIV is full-width regardless
	}
	// The following is a workaround for iOS5 with Mobile Safari (not as a
	// web application), which doesn't deal correctly with "bottom: 0px" for
	// "bottomBar";
	//
	bottomBar.style.top = "" + (wSize.y - bottomBar.offsetHeight) + "px";
}

function scaleOn(wSize) {
	// Show new sizing, for debugging
	//
	timerScaleOn = null;
	if (false) {
		scaling.style.display = 'block';
		var temp = scalingFrag.replace(/<#XPX>/, wSize.x);
		temp = temp.replace(/<#YPX>/, wSize.y);
		scaling.innerHTML = temp;
		moveToCenter(scaling, wSize);
		if (timerScaleOff) clearTimeout(timerScaleOff);
		timerScaleOff = window.setTimeout(scaleOff, 1000);
	}
}

function scaleOff() {
	// Remove size display
	//
	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.

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 (true) {
		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 parents = doc.getElementsByTagName("parent");
	if (parents.length == 0) {
		newPage.parentBtns = parentsTxt; // parent links HTML
		newPage.lastParent = "";
	} else {
		var child = parents[parents.length-1];
		var chtitle = getTitle(child);
		var chpath = child.getAttribute("path");
		var temp = parentFrag.replace(/<#PATH>/g, chpath);
		temp = temp.replace(/<#TITLE>/g, htmlspecials(chtitle));
		newPage.parentBtns = 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.<p>" +
				htmlspecials(raw);
			newPage.isPhoto = false;
			newPage.subFolders = new Array();
		} 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;
		newPage.autoImage = doc.getAttribute("first");
		newPage.autoRoot = newPage.path; // use self for folders
		//
		// First the sub-folders.  We manufacture a TD element for each,
		// which displayPage will assemble into a suitable set of columns,
		// when it knows the real-time window width.
		//
		var subFolders = new Array();
		var children = doc.getElementsByTagName("folder");
		var folderCount = children.length;
		for (var i = 0; i < folderCount; i++) {
			var subFolder = children[i];
			var chpath = subFolder.getAttribute("path");
			var chtitle = getTitle(subFolder);
			var width = subFolder.getAttribute("width");
			var height = subFolder.getAttribute("height");
			var src = subFolder.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));
			subFolders[subFolders.length] = temp;
		}
		newPage.subFolders = subFolders;
		//
		// Next the child photographs.  We assemble HTML which is a
		// sequence of IMG, which the browser will lay out appropriately.
		//
		var photoMaxW = parseInt(doc.getAttribute("photoMaxW"));
		var xMargin = 6;
		var yMargin = 8;
		photoMaxW = Math.floor(photoMaxW / iconDownscale);
		xMargin = Math.floor(xMargin / iconDownscale);
		var content = "";
		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"));
			width = Math.floor(width / iconDownscale);
			height = Math.floor(height / iconDownscale);
			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 = xMargin + 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>/,
									xMargin+photoMaxW-(width+lMargin));
			temp = temp.replace(/<#MARGINT>/, yMargin);
			temp = temp.replace(/<#MARGINB>/, 0);
			content += temp;
		}
		newPage.html = content;
		newPage.commentary =
			(folderCount > 0 ? "" + folderCount + " folders" : "&nbsp;") +
			(folderCount > 0 && photoCount > 0 ? " and" : "") +
			"<br>" +
			(photoCount > 0 ? "" + photoCount + " photographs" : "&nbsp;")
	}
	return newPage;
}

function displayPage() {
	// Update the display based on the current page attributes
	//
	var date = new Date();
	date.setTime(date.getTime() + 60*60*1000);
	setCookie("AndrewAlbumApp", thisPage.path, date);
	if (thisPage.isPhoto) {
		swiper.cur.innerHTML = thisPage.html;
		var mainElt = document.getElementById("mainWrapper");
		var bkgd = getActualStyle(mainElt).backgroundColor;
		swiper.cur.style.backgroundColor = bkgd;
		if (!timer) startMenubarTimer();
	} else {
		var subContent = "<div class=listing>";
		var subFolders = thisPage.subFolders;
		var folderCount = subFolders.length;
		var nCols = Math.floor(windowSize().x / 320);
		if (nCols == 0) nCols = 1;
		var nRows = Math.ceil(folderCount / nCols);
		for (var row = 0; row < nRows; row++) {
			subContent += (row == 0 ? "<table class=catalog>" : "</tr>");
			subContent += "<tr>";
			for (var col = 0; col < nCols; col++) {
				var n = col * nRows + row;
				if (n >= folderCount) break;
				subContent += subFolders[n];
			}
		}
		if (nRows > 0) subContent += "</tr></table>";
		swiper.cur.innerHTML = subContent + thisPage.html + "</div>";
		swiper.cur.style.backgroundColor = null; // inherit
		swiper.cur.scrollTop = 0;
		showHideMenubars(true);
	}
	var commentary = document.getElementById("commentary");
	if (thisPage.commentary) {
		commentary.innerHTML = thisPage.commentary;
	} else {
		commentary.innerHTML = "&nbsp;<br>&nbsp;";
	}
	title.innerHTML = (thisPage.title == "" ? "&nbsp;" :
								htmlspecials(thisPage.title));
	parentBtns.innerHTML = thisPage.parentBtns;
	editTitle.value = thisPage.title;
	if (dlogs.editForm.style.display == 'block') {
		editInner.style.visibility = "visible";
		editTitle.focus();
		editTitle.select();
	}
	scaleMainImage();
	document.getElementById("linkAnchor").href = '#' + thisPage.path;
	endOfReading();
}


//
// Page access
//

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

function Getter() {
	// Constructor for HTTP requests
}

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

Getter.prototype.handleResult = function(xmlhttp) {
	// Completion procedure: interpret the result, assumed to be XML
	//
	var responseXML = xmlhttp.responseXML;
	if (!responseXML) alert(xmlhttp.responseText);
	var doc = responseXML.documentElement;
	if (doc.nodeName == "save") {
		// Result of editSave
		var status = doc.getAttribute("status");
		var path = doc.getAttribute("path");
		if (this.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 == this.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 +'"');
		handleFailure(this);
	}
}

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 nothing, 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 getter = new Getter();
		getter.requestedPath = path;
		fetching[path] = getter;
		getter.url = "photos.php?op=xml&path=" + path + pdaImageSize;
		initiateXMLHttp(getter);
	}
}

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
	if (thisPage.path == autoRoot) manual();
	displayPage();
	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 : autoInterval));
}

function jumpTo(path) {
	// Jump to given path
	if (timer) clearTimeout(timer);
	var cached = getXML(path, true);
	if (cached) {
		cacheHits++;
		loadPage(cached);
	} else {
		cacheMisses++;
	}
	refreshMenubar();
	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  photoClick() {
	// Click inside a phtoograph
	//
	showHideMenubars(!menubarsVisible);
	if (!timer) startMenubarTimer();
	return false;
}

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 startAuto() {
	// internal subroutine for "auto"
	//
	jumpTo(thisPage.autoImage);
}

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(startAuto, 50);
	btns.auto.hide();
	btns.pause.hide();
	btns.stop.hide();
	btns.resume.hide();
	if (menubarsAuto) showHideMenubars(false);
	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.hide();
	showHideMenubars(true);
	startMenubarTimer();;
	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.hide();
	if (menubarsAuto) showHideMenubars(false);
	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.hide();
	showHideMenubars(true);
	startMenubarTimer();
	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 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, windowSize());
		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 getter = new Getter();
	getter.isPhoto = thisPage.isPhoto;
	getter.url = "photos.php";
	getter.postData = "path=" + thisPage.path + "&op=xmlSave&title=" +
			encodeURIComponent(title) +
			"&user=" + encodeURIComponent(user) +
			"&time=" + nowSec +
			"&hmac=" + hmac;
	initiateXMLHttp(getter);
	return false;
}

function init() {
	reading = document.getElementById("reading");
	scaling = document.getElementById("scaling");

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

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

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

	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");
	new Button("pause", "pause()",
		"Suspend the slideshow, remembering the starting position");
	btns.pause.hide();
	new Button("resume", "resume()",
		"Resume the slideshow");
	btns.resume.hide();
	new Button("stop", "manual()",
		"Stop the slideshow");
	btns.stop.hide();
	new Button("prev", "doPrev()",
		"Move to the previous photo",
		"Move to the previous folder");
	new Button("next", "doNext()",
		"Move to the next photo",
		"Move to the next folder, including sub-folders");
	new Button("skip", "doSkip()",
		"Skip the remainder of this folder",
		"Move to the next folder, skipping this folder's sub-folders");
	new Button("edit", "openDlog('editForm')",
		"Edit this photo's title",
		"Edit this folder's title");
	new Button("help", "openDlog('about')",
		"Learn about this program");
	btns.help.enable();

	swipeInit("contents1", "contents2", "contents3");
	
	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 = "";

	// Enable the UI
	if (windowSize().x <= 640) { // adjustments for small screens
		menubarsAuto = true;
		iconDownscale = 2;
		pdaImageSize = "&pda=";
	}
	document.getElementById("parentBtns").style.visibility = "visible";
	document.getElementById("navigationBtns").style.visibility = "visible";
	document.getElementById("privBtns").style.visibility = "visible";
	window.onresize = doResize;
	window.onorientationchange = doResize;

	jumpTo(dest);
}

