// Lame browser detection
var ua = navigator.userAgent.toLowerCase();
var ieSucks = undefined;
if (ua.indexOf('msie') != -1 && ua.indexOf('opera') == -1) {
    ieSucks = 1;
}

function debug(aMsg) {
    // Print a debug message that shows up in Firefox's Javascript console.
    return;
    window.setTimeout(function() { throw new Error("[debug] " + aMsg); }, 0);
}

function SlideShow(options) {
    // A SlideShow object is a reference to a div in which the user wants to
    // show pictures, a collection of pictures to display, and a group of
    // functions for making them display.

    this.Buffer = function(bufferNum) { // {{{
        // A Buffer is an object that contains the image to load, the HTML element
        // to put it into, and the functions needed to compose the HTML element.

        debug('Buffer');

        this.build = function(slide) { // {{{
            // Given a slide, compose a div element with the correct size of the
            // image inside it.

            debug('build ' + this.bufferNum + ' ' + slide.filename);

            // This is where filemanager files live.
            var FILES = ROOT + 'files/';

            // If we're just starting to build a new buffer, then it's not ready
            // yet, so set complete to false.
            this.complete = false;

            // Temporarily disable onresize event because building a new slide
            // can trigger that.
            thisSlideShow.ignoreResize = true;

            // Clear the buffer.
            while (this.element.hasChildNodes()) {
                this.element.removeChild(this.element.firstChild);
            }

            // Pick the image and put it into the div.
            var chosenSize = this.chooseSize(slide)

            // Center the image within the slide show.
            var dims = thisSlideShow.getDims();
            var left = dims.x - slide.sizes[chosenSize].x;
            var top = dims.y - thisSlideShow.controls.height() - slide.sizes[chosenSize].y;

            // TODO: This allows for negative top/left position, which might screw up
            // some things, such as a full-screen scrollable slidshow.  It
            // would be a good idea to add an option to disallow negative margins

            // Set up an image object so we can pre-load images.
            this.image = document.createElement('img');
            this.image.onload = this.imageOnload;
            this.image.src = FILES + slide.sizes[chosenSize].dir + slide.filename;
            this.image.width = slide.sizes[chosenSize].x;
            this.image.height = slide.sizes[chosenSize].y;
            this.element.appendChild(this.image);

            // Treat the bottom/right the same as the top/left WRT centering.
            // TODO: See note above about negative margins
            this.image.style.marginLeft = Math.floor(left / 2) + 'px';
            this.image.style.marginTop = Math.floor(top / 2) + 'px';
            this.image.style.marginRight = Math.ceil(left / 2) + 'px';
            if (!options.noMarginBottom)
                this.image.style.marginBottom = Math.ceil(top / 2) + 'px';

            // Call user-defined callback, letting the user modify the div.
            if (this.userBuild != undefined) {
                this.userBuild(slide, options, slide.sizes[chosenSize]);
            }

            // Done building slide, so re-enable onresize event.
            thisSlideShow.ignoreResize = false;
        } // }}}

        this.chooseSize = function(slide) { // {{{
            // Gets the dimensions of the slide show element and picks the size of
            // the given slide image that fits inside the slide show.

            debug('chooseSize');

            // Check the mode that the slide show is running in.
            var zoom = thisSlideShow.zoomLevels[0];

            // Get slide show dimensions.
            var dims = thisSlideShow.getDims();
            dims.y -= thisSlideShow.controls.height() * 2;

            switch (zoom) {
                case 'full':
                    // Loop through sizes until we find the original size.
                    for (var i = 0; i < slide.sizes.length; i++) {
                        if (slide.sizes[i].dir == '') {
                            // We found the size we want, so get out of here
                            return i;
                        }
                    }
                break;
                case 'maxfill':
                    // Find a size that will completely fill up the
                    for (var i = 0; i < slide.sizes.length; i++) {
                        if (slide.sizes[i].x == dims.x && slide.sizes[i].y >= dims.y ||
                                slide.sizes[i].y == dims.y && slide.sizes[i].x >= dims.x) {
                            // We found the size we want, so get out of here
                            return i;
                        }
                    }
                break;
                case 'fitx':
                    // Loop through sizes for the passed slide image,
                    // comparing each size to the slide show width.
                    for (var i = 0; i < slide.sizes.length; i++) {
                        if (slide.sizes[i].x <= dims.x) {
                            // We found the size we want, so get out of here
                            return i;
                        }
                    }
                break;
                case 'fity':
                    // Loop through sizes for the passed slide image,
                    // comparing each size to the slide show width.
                    for (var i = 0; i < slide.sizes.length; i++) {
                        if (slide.sizes[i].y <= dims.y) {
                            // We found the size we want, so get out of here
                            return i;
                        }
                    }
                break;
                default:
                case 'fitxy':
                    // Loop through sizes for the passed slide image,
                    // comparing each size to the slide show dimensions.
                    for (var i = 0; i < slide.sizes.length; i++) {
                        if (slide.sizes[i].x <= dims.x
                                && slide.sizes[i].y <= dims.y) {
                            // We found the size we want, so get out of here
                            return i;
                        }
                    }
                break;
            }

            // No image was found that could fit inside the slide show, so
            // return the last image size we found.
            return (i - 1);
        } // }}}

        this.imageOnload = function() { // {{{
            // When the image is finished loading, we tell somebody.

            debug('image.onload');

            // Keep track of the fact that this buffer is complete.
            thisBuffer.complete = true;

            // Let the slide show know we're done loading so it can do whatever it
            // needs to do.
            thisSlideShow.buffered(thisBuffer.bufferNum);
        } // }}}

        // Make a local copy of the special "this" variable so we can use it
        // in places where "this" doesn't point to this buffer, e.g. the image's
        // onload function.
        var thisBuffer = this;

        // Keep track of which buffer we are and which slide show we belong to.
        this.bufferNum = bufferNum;

        // Create the div that the image and any other elements (e.g. captions)
        // will go into for this slide. By default, this div is on top and opaque.
        this.element = document.createElement('div');
        this.element.className = 'buffer' + this.bufferNum;
        this.element.style.position = 'absolute';
        this.element.style.zIndex = thisSlideShow.zTop;
        thisSlideShow.setOpacity(this.element, 1);

        if (this.bufferNum == thisSlideShow.hidBuffer) {
            // If the buffer being created is the hidden buffer, then go to the
            // back.
            this.element.style.zIndex = thisSlideShow.zBottom;
            thisSlideShow.setOpacity(this.element, 0);
        }

    } // }}}

    this.StatusBar = function() { // {{{
        // Object storing the status bar's behavior and composition.

        this.set = function(text) {
            // Set status bar's text.

            var text = document.createTextNode(text);
            thisBar.element.replaceChild(text, thisBar.element.firstChild);
        }

        // Set up the HTML element that will serve as the status bar.
        var thisBar = this;
        this.element = document.createElement('div');
        this.element.appendChild(document.createTextNode(''));
        this.element.className = 'statusBar';
    } // }}}

    this.Counter = function() { // {{{
        // Object storing the counter's behavior and composition.

        this.update = function() {
            // Set counter's text.

            var text = (thisSlideShow.j + 1) + '/' + thisSlideShow.slides.length;
            text = document.createTextNode(text);
            thisCounter.element.replaceChild(text, thisCounter.element.firstChild);
        }

        // Set up the HTML element that will serve as the status bar.
        var thisCounter = this;
        this.element = document.createElement('div');
        this.element.appendChild(document.createTextNode(''));
        this.element.className = 'counter';
    } // }}}

    this.btnPlayPause = function() { // {{{
        // Object storing the "Play/Pause" button's behavior and composition.

        debug('btnPlayPause');

        this.action = function() {
            // Function that's called when button is pressed.

            // If slide show is playing, stop the show.
            // Otherwise, if there is more than one slide, play the show.
            if (thisSlideShow.swapInterval) {
                thisButton.element.replaceChild(thisButton.playText,
                        thisButton.pauseText);
                thisButton.element.title = thisButton.playTitle;
                thisSlideShow.stop();
                thisSlideShow.controls.status.set('Slide show paused.');
                thisButton.icon.src = ROOT + 'images/slideshow/icon-play.gif';
            } else if (thisSlideShow.slides.length > 1) {
                thisButton.element.replaceChild(thisButton.pauseText,
                        thisButton.playText);
                thisButton.element.title = thisButton.pauseTitle;
                thisSlideShow.play();
                thisSlideShow.controls.status.set('Playing slide show...');
                thisButton.icon.src = ROOT + 'images/slideshow/icon-pause.gif';
            }
        }

        // Set up the HTML element that will serve as this button.
        var thisButton = this;
        this.playText = document.createTextNode('Play');
        this.pauseText = document.createTextNode('Pause');
        this.playTitle = 'Play Slide Show';
        this.pauseTitle = 'Pause Slide Show';
        this.element = document.createElement('button');

        this.icon = document.createElement('img');
        this.icon.alt = this.pauseTitle;
        this.icon.src = ROOT + 'images/slideshow/icon-pause.gif';
        this.icon.style.verticalAlign = 'bottom';
        this.icon.width = 18;
        this.icon.height = 18;
        this.element.appendChild(this.icon);

        this.element.className = 'btnPlayPause';
        this.element.appendChild(this.pauseText);
        this.element.title = thisButton.pauseTitle;
        this.element.onclick = this.action;
    } // }}}

    this.btnNextSlide = function() { // {{{
        // Object storing the "Next" button's behavior and composition.

        debug('btnNextSlide');

        this.action = function() {
            // Function that's called when button is pressed.

            // Stop slide show, fetch next slide, build it in the active
            // buffer, and resume slide show.
            var wasPlaying = false;
            if (thisSlideShow.swapInterval) {
                wasPlaying = true;
                thisSlideShow.stop();
            }
            thisSlideShow.increment();
            thisSlideShow.j = thisSlideShow.i;
            thisSlideShow.rebuild();
            if (wasPlaying) {
                thisSlideShow.play();
            }
        }

        // Set up the HTML element that will serve as this button.
        this.element = document.createElement('button');

        this.icon = document.createElement('img');
        this.icon.alt = this.pauseTitle;
        this.icon.src = ROOT + 'images/slideshow/icon-next.gif';
        this.icon.style.verticalAlign = 'bottom';
        this.icon.width = 18;
        this.icon.height = 18;
        this.element.appendChild(this.icon);

        this.element.className = 'btnNextSlide';
        this.element.title = 'Next Slide';
        this.element.appendChild(document.createTextNode('Next'));
        this.element.onclick = this.action;
    } // }}}

    this.btnPrevSlide = function() { // {{{
        // Object storing the "Prev" button's behavior and composition.

        debug('btnPrevSlide');

        this.action = function() {
            // Function that's called when button is pressed.

            // Stop slide show, fetch next slide, build it in the active
            // buffer, and resume slide show.
            var wasPlaying = false;
            if (thisSlideShow.swapInterval) {
                wasPlaying = true;
                thisSlideShow.stop();
            }
            thisSlideShow.decrement();
            thisSlideShow.j = thisSlideShow.i;
            thisSlideShow.rebuild();
            if (wasPlaying) {
                thisSlideShow.play();
            }
        }

        // Set up the HTML element that will serve as this button.
        this.element = document.createElement('button');

        this.icon = document.createElement('img');
        this.icon.alt = this.pauseTitle;
        this.icon.src = ROOT + 'images/slideshow/icon-prev.gif';
        this.icon.style.verticalAlign = 'bottom';
        this.icon.width = 18;
        this.icon.height = 18;
        this.element.appendChild(this.icon);

        this.element.className = 'btnPrevSlide';
        this.element.title = 'Previous Slide';
        this.element.appendChild(document.createTextNode('Prev'));
        this.element.onclick = this.action;
    } // }}}

    this.btnZoomToggle = function() { // {{{
        // Object storing the "Zoom" button's behavior and composition.

        debug('btnZoomToggle');

        this.action = function() {
            // Function that's called when button is pressed.

            // Stop slide show, fetch current slide, build it in the active
            // buffer, and resume slide show.
            var wasPlaying = false;
            if (thisSlideShow.swapInterval) {
                wasPlaying = true;
                thisSlideShow.stop();
            }
            thisSlideShow.zoomLevels.push(thisSlideShow.zoomLevels.shift());
            var nextZoomLevel = thisSlideShow.zoomLevels[0];
            if (thisSlideShow.zoomLevels.length > 1) {
                nextZoomLevel = thisSlideShow.zoomLevels[1];
            }
            thisButton.element.title = thisButton.titles[nextZoomLevel];
            thisButton.element.replaceChild(thisButton.textNodes[nextZoomLevel],
                    thisButton.element.firstChild);
            thisSlideShow.rebuild();
            if (wasPlaying) {
                thisSlideShow.play();
            }
        }

        // Set up the HTML element that will serve as this button.
        var thisButton = this;
        this.textNodes = {
            'fitxy': document.createTextNode('Zoom: Best Fit'),
            'maxfill': document.createTextNode('Zoom: Fill With Cropping'),
            'fitx': document.createTextNode('Zoom: Fit Width'),
            'fity': document.createTextNode('Zoom: Fit Height'),
            'full': document.createTextNode('Zoom: Full Size')
        }
        this.titles = {
            'fitxy': 'Zoom to Fit Entire Image',
            'maxfill': 'Zoom to Fill With Cropping',
            'fitx': 'Zoom to Fit Width',
            'fity': 'Zoom to Fit Height',
            'full': 'View Full Size Image'
        }
        this.element = document.createElement('button');
        this.element.className = 'btnZoomToggle';

        var nextZoomLevel = thisSlideShow.zoomLevels[0];
        if (thisSlideShow.zoomLevels.length > 1) {
            nextZoomLevel = thisSlideShow.zoomLevels[1];
        }
        this.element.title = this.titles[nextZoomLevel];
        this.element.appendChild(this.textNodes[nextZoomLevel]);

        this.element.onclick = this.action;
    } // }}}

    this.btnGroupZoomRadios = function() { // {{{
        // Object storing the "Zoom" button group's behavior and composition.

        debug('btnGroupZoomRadios');

        // Set up the HTML element that will serve as this button group.
        var thisButtonGroup = this;
        this.buttons = {
            fitxy: {
                element: document.createElement('button'),
                textNode: document.createTextNode('Best Fit'),
                title: 'Zoom to fit entire image',
                img: 'icon-zoom-fitxy.gif',
                action: function() {
                    thisButtonGroup.activate('fitxy');
                    thisSlideShow.setZoomLevel('fitxy');
                    thisSlideShow.rebuild();
                }
            },
            maxfill: {
                element: document.createElement('button'),
                textNode: document.createTextNode('Fill with Cropping'),
                title: 'Fill with Cropping',
                img: 'icon-zoom-fitxy.gif',
                action: function() {
                    thisButtonGroup.activate('maxfill');
                    thisSlideShow.setZoomLevel('maxfill');
                    thisSlideShow.rebuild();
                }
            },
            fitx: {
                element: document.createElement('button'),
                textNode: document.createTextNode('Fit Width'),
                title: 'Zoom to fit image width',
                img: 'icon-zoom-fitx.gif',
                action: function() {
                    thisButtonGroup.activate('fitx');
                    thisSlideShow.setZoomLevel('fitx');
                    thisSlideShow.rebuild();
                }
            },
            fity: {
                element: document.createElement('button'),
                textNode: document.createTextNode('Fit Height'),
                title: 'Zoom to fit image height',
                img: 'icon-zoom-fity.gif',
                action: function() {
                    thisButtonGroup.activate('fity');
                    thisSlideShow.setZoomLevel('fity');
                    thisSlideShow.rebuild();
                }
            },
            full: {
                element: document.createElement('button'),
                textNode: document.createTextNode('Full Size'),
                title: 'View full size image',
                img: 'icon-zoom-full.gif',
                action: function() {
                    thisButtonGroup.activate('full');
                    thisSlideShow.setZoomLevel('full');
                    thisSlideShow.rebuild();
                }
            }
        };

        this.activate = function(actButtonName) {
            // Set a new button as the pressed button, setting all others to
            // non-pressed.

            for (buttonName in this.buttons) {
                if (buttonName == actButtonName)
                    this.buttons[buttonName].element.className = 'pressed';
                else
                    this.buttons[buttonName].element.className = '';
            }
        }

        this.element = document.createElement('div');
        this.element.className = 'zoomRadios';

        // Add label before buttons.
        var label = document.createElement('label');
        label.appendChild(document.createTextNode('Zoom: '));
        this.element.appendChild(label);

        // Loop through zoom levels, adding a button to the group for each
        // zoom level.
        var htmlImg;
        var button;
        for (var i = 0; i < thisSlideShow.zoomLevels.length; i++) {
            button = this.buttons[thisSlideShow.zoomLevels[i]];
            if (i == 0) button.element.className = 'pressed';
            htmlImg = document.createElement('img');
            htmlImg.width = 19;
            htmlImg.height = 19;
            htmlImg.src = ROOT + 'images/slideshow/' + button.img;
            htmlImg.alt = button.title;
            button.element.appendChild(htmlImg);
            button.element.appendChild(button.textNode);
            button.element.title = button.title;
            button.element.onclick = button.action;
            this.element.appendChild(button.element);
        }
    } // }}}

    this.btnMute = function(mp3Element) { // {{{
        // Object storing the "Mute" button's behavior and composition.

        debug('btnMute');

        this.action = function() {
            // Function that's called when the button is pressed.

            if (!player) return false;

            // If player is playing, stop it. Otherwise, play it.
            if (player.TCurrentFrame('/') == 0) {
                thisButton.element.replaceChild(thisButton.unmuteText,
                        thisButton.muteText);
                thisButton.element.title = thisButton.unmuteTitle;
                player.GotoFrame(1);
                thisButton.icon.src = ROOT + 'images/slideshow/icon-unmute.gif';
            } else {
                thisButton.element.replaceChild(thisButton.muteText,
                        thisButton.unmuteText);
                thisButton.element.title = thisButton.muteTitle;
                player.GotoFrame(0);
                thisButton.icon.src = ROOT + 'images/slideshow/icon-mute.gif';
            }
        }

        // Set up the HTML element that will serve as this button.
        var thisButton = this;
        var player = mp3Element;
        this.muteText = document.createTextNode('Mute');
        this.unmuteText = document.createTextNode('Unmute');
        this.muteTitle = 'Mute Sound';
        this.unmuteTitle = 'Unmute Sound';
        this.element = document.createElement('button');

        this.icon = document.createElement('img');
        this.icon.alt = this.pauseTitle;
        this.icon.src = ROOT + 'images/slideshow/icon-mute.gif';
        this.icon.style.verticalAlign = 'bottom';
        this.icon.width = 18;
        this.icon.height = 18;
        this.element.appendChild(this.icon);

        this.element.className = 'btnMute';
        this.element.appendChild(this.muteText);
        this.element.title = thisButton.muteTitle;
        this.element.onclick = this.action;
    } // }}}

        this.btnPrint = function() { // {{{
            // Object storing the "Print" button's behavior and composition.

            debug('btnPrint');

            this.action = function() {
                // Function that's called when button is pressed.

                // If slide show is playing, stop the show.
                if (thisSlideShow.swapInterval) {
                    thisSlideShow.controls.playpause.action();
                }

                // Open a new window in which to print the slide.
                window.open(ROOT + 'prn/' +
                        thisSlideShow.slides[thisSlideShow.j].file_id + '/',
                        'printWindow');
            }

            // Set up the HTML element that will serve as this button.
            var thisButton = this;
            this.printText = document.createTextNode('Print');
            this.printTitle = 'Print This Image';
            this.element = document.createElement('button');

            this.icon = document.createElement('img');
            this.icon.alt = this.printTitle;
            this.icon.src = ROOT + 'images/slideshow/icon-print.gif';
            this.icon.style.verticalAlign = 'bottom';
            this.icon.width = 18;
            this.icon.height = 18;
            this.element.appendChild(this.icon);

            this.element.className = 'btnPrint';
            this.element.appendChild(this.printText);
            this.element.title = thisButton.printTitle;
            this.element.onclick = this.action;
        } // }}}

    this.btnClose = function() { // {{{
        // Object storing the "Close" button's behavior and composition.

        debug('btnClose');

        this.action = function() {
            // Function that's called when button is pressed.

            // Stop slide show.
            if (thisSlideShow.swapInterval) {
                thisSlideShow.stop();
            }

            // Close the window if possible, otherwise go back.
            if (window.opener) {
                window.close();
            } else if (window.history.length) {
                window.history.back();
            }
        }

        // Set up the HTML element that will serve as this button.
        this.element = document.createElement('button');

        this.icon = document.createElement('img');
        this.icon.alt = this.pauseTitle;
        this.icon.src = ROOT + 'images/slideshow/icon-close.gif';
        this.icon.style.verticalAlign = 'bottom';
        this.icon.width = 18;
        this.icon.height = 18;
        this.element.appendChild(this.icon);

        this.element.className = 'btnClose';
        this.element.title = 'Close Slide Show';
        this.element.appendChild(document.createTextNode('Close'));
        this.element.onclick = this.action;
    } // }}}

    this.intervalFunc = function() { // {{{
        // Called each second to check the status of the slide show and switch
        // to the next slide if ready.

        debug('intervalFunc ' + this.seconds);

        // If it's been 8 seconds since the last slide and the hidden buffer
        // has finished loading, swap the buffers and reset the counter.
        this.seconds += this.fracDelay;
        if (this.seconds >= this.delay &&
                this.buffers[this.hidBuffer].complete) {
            this.transFade(this.buffers[this.actBuffer],
                    this.buffers[this.hidBuffer]);
            this.seconds = 0;
        }
    } // }}}

    this.play = function() { // {{{
        // Engage play mode.

        debug('play');

        // Create an interval to run intervalFunc, which checks on the status
        // of the slide show every second to see if we're ready to go to the
        // next slide.
        this.swapInterval = window.setInterval(
            function() {
                thisSlideShow.intervalFunc()
            }, thisSlideShow.fracDelay * 1000
        );
    } // }}}

    this.stop = function() { // {{{
        // Disengage play mode.

        debug('stop');

        // Stop calling intervalFunc, without which nothing will happen.
        clearInterval(this.swapInterval);
        this.swapInterval = 0;
        this.seconds = 0;
    } // }}}

    this.seek = function(seekTo) { // {{{
        // Safely seek to a given slide, with end-wrapping.

        debug('seek');

        // Wrap value of seekTo so it doesn't seek past the ends of the show.
        // Javascript's modulus returns a negative if its first operand is
        // negative, so we have to fix it:
        var seekTo = seekTo % this.slides.length;
        if (seekTo < 0) seekTo += this.slides.length;

        this.i = seekTo;
        return seekTo;
    } // }}}

    this.increment = function() { // {{{
        // Safely increment i, which is the current slide index.

        debug('increment');

        return this.seek(this.j + 1);
    } // }}}

    this.decrement = function() { // {{{
        // Safely decrement i, which is the current slide index.

        debug('decrement');

        return this.seek(this.j - 1);
    } // }}}

    this.buffered = function(bufferNum) { // {{{
        // Deal with notifications about images being finished loading.

        debug('buffered ' + bufferNum + ', ' + this.actBuffer);

        // If the buffer that just finished was the active (i.e. visible)
        // buffer, then make the hidden buffer load the next image.
        if (bufferNum == this.actBuffer) {
            this.bufferNext();
        }
    } // }}}

    this.bufferNext = function() { // {{{
        // Get another slide ready to display.

        debug('bufferNext');

        // Start building the next slide in the hidden buffer and increment
        // the slide index.
        if (this.hidBuffer != undefined) {
            this.buffers[this.hidBuffer].build(this.options.slides[this.increment()]);
        }
    } // }}}

    this.rebuild = function() { // {{{
        // Rebuild current frame in active buffer.

        var newSlide = this.slides[this.j];
        this.controls.counter.update();
        this.buffers[this.actBuffer].build(newSlide);
    } // }}}

    this.transJumpCut = function(from, to) { // {{{
        // Instantly switch from one slide to the next.

        debug('transJumpCut');

        // Move new slide to top and old slide to bottom.
        to.element.style.zIndex = this.zTop;
        to.element.style.display = 'block';
        from.element.style.zIndex = this.zBottom;

        // Make new slide visible and old slide invisible.
        this.setOpacity(to.element, 1);
        this.setOpacity(from.element, 0);

        // Do post-transition stuff.
        this.fadeDone();
    } // }}}

    this.transFade = function(from, to) { // {{{
        // Dissolve from one slide to the next.

        debug('transFade');

        // Make sure the slide that's going to fade in is transparent and
        // above the old slide.
        this.setOpacity(to.element, 0);
        to.element.style.display = 'block';
        to.element.style.zIndex = this.zTop;
        from.element.style.zIndex = this.zBottom;

        // Execute fade, keeping track of how long each one will take.
        var tFrom = this.fade(from.element, 0);
        var tTo = this.fade(to.element, 1);

        // Figure out which fade will take longer and use that.
        var t = tFrom;
        if (tTo > t) t = tTo;

        // Set one last timeout to execute after the last opacity timeout.
        // This one will signal the slide show that the fade is complete.
        var toid = window.setTimeout(function() {
            thisSlideShow.fadeDone();
        }, 50 * t);
        this.fadeTimeouts.push(toid);
    } // }}}

    this.fade = function(element, destOpac) { // {{{
        // Gradually change an element's opacity to another value.

        debug('fade');

        // Create a function that returns a function that will set the fading
        // slide's opacity to what we want. This is necessary because
        // otherwise we'd be putting a closure in a loop, which is Bad.
        function setFadeTimeout(element, newOpac) {
            return function() { thisSlideShow.setOpacity(element, newOpac); }
        }

        // Define beginning and end points of fade.
        var startOpac = Number(element.style.opacity);
        var destOpac = Number(destOpac);

        // t is a simple counter that will be used to set increasing timeout
        // durations for setTimeout(), creating animation.
        var t = 0;

        // Loop from startOpac to destOpac, each time setting newOpac to a
        // weighted average of newOpac and destOpac.
        var toid = 0;
        for (var newOpac = startOpac;
                Math.abs(newOpac - destOpac) > 0.01;
                newOpac = (newOpac * 4 + destOpac) / 5) {

            // Set a timeout to change the opacity and put a reference to the
            // timeout into the fadeTimeouts array.
            toid = window.setTimeout(setFadeTimeout(element, newOpac), 50 * t);
            this.fadeTimeouts.push(toid);

            // Increment timeout duration counter.
            t++;
        }

        // Let the caller function know how many frames the fade will require.
        return t;
    } // }}}

    this.fadeDone = function() { // {{{
        // Clean up when a fade is complete.

        debug('fadeDone');

        // Change the SlideShow object's references so actBuffer refers to the
        // newly activated buffer and hidBuffer refers to the newly hidden
        // buffer.
        var temp = this.actBuffer;
        this.actBuffer = this.hidBuffer;
        this.hidBuffer = temp;
        this.j = this.i;
        this.controls.counter.update();

        // Clear all existing fade timeouts.
        while (this.fadeTimeouts.length) {
            var toid = this.fadeTimeouts.shift();
            window.clearTimeout(toid);
        }

        // Make sure the active buffer is visible and the hidden buffer is
        // invisible.
        this.setOpacity(this.buffers[this.actBuffer].element, 1);
        this.setOpacity(this.buffers[this.hidBuffer].element, 0);
        this.buffers[this.hidBuffer].element.style.display = 'none';

        // Start loading the next slide in the hidden buffer.
        this.bufferNext();
    } // }}}

    this.setOpacity = function (element, opacity) { // {{{
        // Set element opacity in multiple browsers.

        element.style.opacity = opacity;
        element.style.MozOpacity = opacity;
        element.style.KhtmlOpacity = opacity;
        if (ieSucks)
            element.style.filter = "alpha(opacity=" + (opacity * 100) + ")";
    } // }}}

    this.getDims = function() { // {{{
        // Get width and height of passed element.

        var dims = {
            x: this.element.clientWidth,
            y: this.element.clientHeight
        }

        if (this.saveSpaceX !== undefined)
            dims.x = dims.x - this.saveSpaceX;
        if (this.saveSpaceY !== undefined)
            dims.y = dims.y - this.saveSpaceY;

        return dims;
    } // }}}

    this.setZoomLevel = function(newZoomLevel) { // {{{
        // Prepend the passed zoom level value to the zoomLevels array, which
        // makes that the active zoom level. Then append the old zoom levels,
        // removing any old instances of the newly-set zoom level.

        var newZoomLevels = [ newZoomLevel ];
        var zoomLevel;
        while (this.zoomLevels.length) {
            zoomLevel = this.zoomLevels.shift();
            if (zoomLevel != newZoomLevel) newZoomLevels.push(zoomLevel);
        }
        this.zoomLevels = newZoomLevels;
    } // }}}

    debug('SlideShow');

    // Don't create a show if there are no slides.
    if (!options.slides.length)
        return false;

    // Make a local copy of the special "this" variable so we can use it
    // in places where "this" doesn't point to this slide show, e.g. in
    // timeout/interval functions and button onclick events.
    var thisSlideShow = this;

    this.options = options;

    // Find out where we live in the HTML.
    this.element = document.getElementById(this.options.id);

    // Get our slides.
    this.slides = this.options.slides;

    // Pass along the user's optional buffer-modification function that allows
    // for custom content like captions to appear in slides.
    if (this.options.builder != undefined
            && this.options.builder instanceof Function) {
        this.Buffer.prototype.userBuild = this.options.builder;
    }

    // How should image sizes be chosen? The zoom levels in this array will be
    // cycled through when the Zoom button is clicked. zoomLevels[0] is used as
    // the current zoom level. Possible values:
    // 'full': original, full-size image
    // 'maxfill': Fill the slideshow area with cropping in one direction (no blank space)
    // 'fitx': largest image that will fit the width of the slide show
    // 'fity': largest image that will fit the height of the slide show
    // 'fitxy': largest image that will completely fit within the slide show
    if (this.options.zoomLevels) {
        this.zoomLevels = this.options.zoomLevels;
    } else {
        this.zoomLevels = ['fitxy', 'full'];
    }

    // For saving space (e.g. for a caption)
    if (this.options.saveSpaceX) {
        this.saveSpaceX = this.options.saveSpaceX;
    } else {
        this.saveSpaceX = 0;
    }

    if (this.options.saveSpaceY) {
        this.saveSpaceY = this.options.saveSpaceY;
    } else {
        this.saveSpaceY = 0;
    }

    // Make sure buffer divs can have absolute positioning relative to the
    // slide show.
    this.element.style.position = 'relative';
    this.zBottom = (Number(this.element.style.zIndex)) + 1;
    this.zTop = this.zBottom + 1;

    // Current slide index. Default = 0, but allow seeking to a given slide.
    this.i = 0;
    this.options.i = Number(this.options.i);
    if (!isNaN(this.options.i)) {
        this.seek(this.options.i);
    }

    // Delay between slides, in seconds.
    // Delay should be at least 1 or so if you're using transFade() as your
    // transition function, or weirdness will ensue.
    this.delay = 5;
    if (this.options.delay != undefined) {
        this.delay = this.options.delay;
    }

    // Fractional delay duration, in seconds. This is how often intervalFunc
    // will be called to check to see if it should switch to the next slide.
    this.fracDelay = this.delay / 10;

    // i keeps track of the last slide built. A separate variable is needed
    // for the slide currently displayed.
    this.j = this.i;

    // Variable to keep track of seconds since last slide change.
    this.seconds = 0;

    // Array to store references to opacity-setting timeout functions.
    this.fadeTimeouts = [];

    // Create controls object, which will hold the controls. {{{
    this.controls = {
        element: document.createElement('div'),
        status: new this.StatusBar(),
        counter: new this.Counter(),
        playpause: new this.btnPlayPause(),
        prev: new this.btnPrevSlide(),
        next: new this.btnNextSlide(),
        zoomtoggle: new this.btnZoomToggle(),
        zoomradios: new this.btnGroupZoomRadios(),
        mute: new this.btnMute(this.options.mp3Element),
        print: new this.btnPrint(),
        close: new this.btnClose(),
        use: function(name) {
            this.element.appendChild(this[name].element);
            // Fix an IE absolute positioning bug:
            this.element.appendChild(document.createTextNode(' '));
        },
        getDims: thisSlideShow.getDims,
        height: function() {
            // If controls are not inside the slide show div,
            // report 0 for height.
            var isInside = false;
            var myParent = this.element.parentNode;
            while (myParent) {
                if (myParent.id == thisSlideShow.element.id) {
                    isInside = true;
                    break;
                }
                myParent = myParent.parentNode;
            }
            if (isInside) {
                return this.getDims().y;
            } else {
                return 0;
            }
        }
    }
    this.controls.element.className = 'controls';
    this.controls.element.style.zIndex = this.zTop + 10;

    // Use controls specified through options if they were specified.
    // Otherwise, If there are two or more slides, run in slideshow mode.
    // Otherwise, run in zooming viewer mode.
    var controlsUsed = [];
    if (this.options.controlsUsed) {
        controlsUsed = this.options.controlsUsed;
    } else if (this.slides.length > 1) {
        controlsUsed = ['status', 'counter', 'playpause', 'prev', 'next', 'mute', 'close'];
    } else if (this.slides.length > 0) {
        controlsUsed = ['status', 'zoomradios', 'close'];
    }
    for (var i = 0; i < controlsUsed.length; i++) {
        if (controlsUsed[i] == 'mute' && this.options.mp3Element == undefined)
            continue;
        this.controls.use(controlsUsed[i]);
    }
    this.element.appendChild(this.controls.element);
    // }}}

    // Make the window pass along its onresize event to the slide show. {{{
    this.ignoreResize = false;
    this.onresize = function(evt) {
        // IE fires onresize events when you click the Zoom button, so we'll
        // ignore those.
        if (thisSlideShow.ignoreResize)
            return;

        // Stop slide show, fetch current slide, build it in the active
        // buffer, and resume slide show.
        var wasPlaying = false;
        if (thisSlideShow.swapInterval) {
            wasPlaying = true;
            thisSlideShow.stop();
        }
        thisSlideShow.rebuild();
        if (wasPlaying) {
            thisSlideShow.play();
        }
    }
    if (window.addEventListener)
        window.addEventListener('resize', this.onresize, false);
    else
        window.attachEvent('onresize', this.onresize);
    // }}}

    // Set up the first buffer, make it active, and build it.
    this.actBuffer = 0;
    this.buffers = [new this.Buffer(this.actBuffer)];
    this.buffers[this.actBuffer].build(this.options.slides[this.i]);

    // Place the active buffer element into the slide show element.
    this.element.appendChild(this.buffers[this.actBuffer].element);

    if (this.slides.length > 1) {
        // If there is more than one slide, create a second buffer, put it
        // into the slide show element, make it hidden, and start playing the
        // slide show.
        this.hidBuffer = 1;
        this.buffers.push(new this.Buffer(this.hidBuffer));
        this.element.appendChild(this.buffers[this.hidBuffer].element);
        this.play();
        this.controls.status.set('Playing slide show...');
        this.controls.counter.update();
    } else {
        this.controls.status.set('Viewing single image.');
    }

    if (!isNaN(this.options.i)) {
        this.controls.playpause.action();
    }

}
